@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
@@ -1,16 +1,20 @@
1
1
  import { Input } from '@tuturuuu/ui/input';
2
+ import { Textarea } from '@tuturuuu/ui/textarea';
2
3
  import { MAX_TASK_NAME_LENGTH } from '@tuturuuu/utils/constants';
3
4
  import { useTranslations } from 'next-intl';
5
+ import { useLayoutEffect, useRef } from 'react';
4
6
  import {
5
7
  getNormalizedCursorPosition,
6
8
  normalizeLiveTextReplacements,
7
9
  normalizeTextReplacements,
8
10
  } from '../../../../text-editor/text-replacements';
9
11
 
12
+ type TaskTitleControlElement = HTMLInputElement | HTMLTextAreaElement;
13
+
10
14
  interface TaskNameInputProps {
11
15
  name: string;
12
16
  isCreateMode: boolean;
13
- titleInputRef: React.RefObject<HTMLInputElement | null>;
17
+ titleInputRef: React.RefObject<TaskTitleControlElement | null>;
14
18
  editorRef: React.RefObject<HTMLDivElement | null>;
15
19
  lastCursorPositionRef: React.RefObject<number | null>;
16
20
  targetEditorCursorRef: React.MutableRefObject<number | null>;
@@ -18,6 +22,8 @@ interface TaskNameInputProps {
18
22
  updateName: (value: string) => void;
19
23
  flushNameUpdate: () => void;
20
24
  disabled?: boolean;
25
+ variant?: 'fullscreen' | 'compact';
26
+ onSubmit?: () => void;
21
27
  }
22
28
 
23
29
  export function TaskNameInput({
@@ -31,10 +37,29 @@ export function TaskNameInput({
31
37
  updateName,
32
38
  flushNameUpdate,
33
39
  disabled,
40
+ variant = 'fullscreen',
41
+ onSubmit,
34
42
  }: TaskNameInputProps) {
35
43
  const t = useTranslations('ws-task-boards.dialog');
44
+ const isCompact = variant === 'compact';
45
+ const hasPlacedInitialCaretRef = useRef(false);
46
+
47
+ useLayoutEffect(() => {
48
+ if (hasPlacedInitialCaretRef.current || disabled) return;
49
+
50
+ const titleInput = titleInputRef.current;
51
+ if (!titleInput) return;
52
+
53
+ hasPlacedInitialCaretRef.current = true;
54
+ const endPosition = titleInput.value.length;
55
+
56
+ titleInput.focus();
57
+ titleInput.setSelectionRange(endPosition, endPosition);
58
+ }, [disabled, titleInputRef]);
36
59
 
37
60
  const focusDescriptionEditor = () => {
61
+ if (isCompact) return;
62
+
38
63
  targetEditorCursorRef.current = null;
39
64
 
40
65
  setTimeout(() => {
@@ -46,114 +71,144 @@ export function TaskNameInput({
46
71
  }, 0);
47
72
  };
48
73
 
74
+ const handleChange = (e: React.ChangeEvent<TaskTitleControlElement>) => {
75
+ const rawValue = e.target.value;
76
+ const normalizedValue = normalizeLiveTextReplacements(rawValue);
77
+
78
+ if (rawValue !== normalizedValue) {
79
+ const rawCursorPosition = e.target.selectionStart ?? rawValue.length;
80
+ const nextCursorPosition = getNormalizedCursorPosition(
81
+ rawValue,
82
+ rawCursorPosition,
83
+ normalizeLiveTextReplacements
84
+ );
85
+
86
+ requestAnimationFrame(() => {
87
+ titleInputRef.current?.setSelectionRange(
88
+ nextCursorPosition,
89
+ nextCursorPosition
90
+ );
91
+ });
92
+ }
93
+
94
+ setName(normalizedValue);
95
+ // Trigger debounced save while typing (in edit mode)
96
+ if (!isCreateMode && normalizedValue.trim()) {
97
+ updateName(normalizedValue);
98
+ }
99
+ };
100
+
101
+ const handleBlur = (e: React.FocusEvent<TaskTitleControlElement>) => {
102
+ const normalizedValue = normalizeTextReplacements(e.target.value);
103
+
104
+ if (normalizedValue !== e.target.value) {
105
+ setName(normalizedValue);
106
+ if (!isCreateMode && normalizedValue.trim()) {
107
+ updateName(normalizedValue);
108
+ }
109
+ }
110
+
111
+ // Flush pending save immediately when user clicks away (in edit mode)
112
+ if (!isCreateMode && normalizedValue.trim()) {
113
+ flushNameUpdate();
114
+ }
115
+ };
116
+
117
+ const handleKeyDown = (e: React.KeyboardEvent<TaskTitleControlElement>) => {
118
+ // Enter key moves to description
119
+ if (
120
+ e.key === 'Enter' &&
121
+ !e.altKey &&
122
+ !e.ctrlKey &&
123
+ !e.metaKey &&
124
+ !e.shiftKey
125
+ ) {
126
+ if (e.nativeEvent.isComposing || e.keyCode === 229) {
127
+ return;
128
+ }
129
+
130
+ e.preventDefault();
131
+ e.stopPropagation();
132
+ // Flush pending save immediately when pressing Enter (in edit mode)
133
+ if (!isCreateMode && e.currentTarget.value.trim()) {
134
+ flushNameUpdate();
135
+ }
136
+ if (isCompact) {
137
+ onSubmit?.();
138
+ return;
139
+ }
140
+ focusDescriptionEditor();
141
+ }
142
+
143
+ if (!isCompact && e.key === 'ArrowDown') {
144
+ e.preventDefault();
145
+ const input = e.currentTarget;
146
+ const cursorPosition = input.selectionStart ?? 0;
147
+
148
+ // Store cursor position for smart navigation
149
+ lastCursorPositionRef.current = cursorPosition;
150
+ targetEditorCursorRef.current = cursorPosition;
151
+
152
+ // Focus the editor - cursor positioning will be handled by the editor via prop
153
+ const editorElement = editorRef.current?.querySelector(
154
+ '.ProseMirror'
155
+ ) as HTMLElement;
156
+ if (editorElement) {
157
+ editorElement.focus();
158
+ }
159
+ }
160
+
161
+ // Right arrow at end of title moves to description
162
+ if (!isCompact && e.key === 'ArrowRight') {
163
+ const input = e.currentTarget;
164
+ const cursorPosition = input.selectionStart ?? 0;
165
+ const textLength = input.value.length;
166
+
167
+ // Only move if cursor is at the end
168
+ if (cursorPosition === textLength) {
169
+ e.preventDefault();
170
+ const editorElement = editorRef.current?.querySelector(
171
+ '.ProseMirror'
172
+ ) as HTMLElement;
173
+ if (editorElement) {
174
+ editorElement.focus();
175
+ }
176
+ }
177
+ }
178
+ };
179
+
180
+ if (isCompact) {
181
+ return (
182
+ <div className="group">
183
+ <Textarea
184
+ ref={titleInputRef as React.RefObject<HTMLTextAreaElement | null>}
185
+ data-task-name-input
186
+ disabled={disabled}
187
+ value={name}
188
+ maxLength={MAX_TASK_NAME_LENGTH}
189
+ rows={1}
190
+ onChange={handleChange}
191
+ onBlur={handleBlur}
192
+ onKeyDown={handleKeyDown}
193
+ placeholder={t('task_name_placeholder')}
194
+ className="max-h-32 min-h-11 resize-none overflow-y-auto border-0 bg-transparent px-0 py-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"
195
+ autoFocus
196
+ />
197
+ </div>
198
+ );
199
+ }
200
+
49
201
  return (
50
202
  <div className="group">
51
203
  <Input
52
- ref={titleInputRef}
204
+ ref={titleInputRef as React.RefObject<HTMLInputElement | null>}
53
205
  data-task-name-input
54
206
  disabled={disabled}
55
207
  value={name}
56
208
  maxLength={MAX_TASK_NAME_LENGTH}
57
- onChange={(e) => {
58
- const rawValue = e.target.value;
59
- const normalizedValue = normalizeLiveTextReplacements(rawValue);
60
-
61
- if (rawValue !== normalizedValue) {
62
- const rawCursorPosition =
63
- e.target.selectionStart ?? rawValue.length;
64
- const nextCursorPosition = getNormalizedCursorPosition(
65
- rawValue,
66
- rawCursorPosition,
67
- normalizeLiveTextReplacements
68
- );
69
-
70
- requestAnimationFrame(() => {
71
- titleInputRef.current?.setSelectionRange(
72
- nextCursorPosition,
73
- nextCursorPosition
74
- );
75
- });
76
- }
77
-
78
- setName(normalizedValue);
79
- // Trigger debounced save while typing (in edit mode)
80
- if (!isCreateMode && normalizedValue.trim()) {
81
- updateName(normalizedValue);
82
- }
83
- }}
84
- onBlur={(e) => {
85
- const normalizedValue = normalizeTextReplacements(e.target.value);
86
-
87
- if (normalizedValue !== e.target.value) {
88
- setName(normalizedValue);
89
- if (!isCreateMode && normalizedValue.trim()) {
90
- updateName(normalizedValue);
91
- }
92
- }
93
-
94
- // Flush pending save immediately when user clicks away (in edit mode)
95
- if (!isCreateMode && normalizedValue.trim()) {
96
- flushNameUpdate();
97
- }
98
- }}
99
- onKeyDown={(e) => {
100
- // Enter key moves to description
101
- if (
102
- e.key === 'Enter' &&
103
- !e.altKey &&
104
- !e.ctrlKey &&
105
- !e.metaKey &&
106
- !e.shiftKey
107
- ) {
108
- if (e.nativeEvent.isComposing || e.keyCode === 229) {
109
- return;
110
- }
111
-
112
- e.preventDefault();
113
- e.stopPropagation();
114
- // Flush pending save immediately when pressing Enter (in edit mode)
115
- if (!isCreateMode && e.currentTarget.value.trim()) {
116
- flushNameUpdate();
117
- }
118
- focusDescriptionEditor();
119
- }
120
-
121
- if (e.key === 'ArrowDown') {
122
- e.preventDefault();
123
- const input = e.currentTarget;
124
- const cursorPosition = input.selectionStart ?? 0;
125
-
126
- // Store cursor position for smart navigation
127
- lastCursorPositionRef.current = cursorPosition;
128
- targetEditorCursorRef.current = cursorPosition;
129
-
130
- // Focus the editor - cursor positioning will be handled by the editor via prop
131
- const editorElement = editorRef.current?.querySelector(
132
- '.ProseMirror'
133
- ) as HTMLElement;
134
- if (editorElement) {
135
- editorElement.focus();
136
- }
137
- }
138
-
139
- // Right arrow at end of title moves to description
140
- if (e.key === 'ArrowRight') {
141
- const input = e.currentTarget;
142
- const cursorPosition = input.selectionStart ?? 0;
143
- const textLength = input.value.length;
144
-
145
- // Only move if cursor is at the end
146
- if (cursorPosition === textLength) {
147
- e.preventDefault();
148
- const editorElement = editorRef.current?.querySelector(
149
- '.ProseMirror'
150
- ) as HTMLElement;
151
- if (editorElement) {
152
- editorElement.focus();
153
- }
154
- }
155
- }
156
- }}
209
+ onChange={handleChange}
210
+ onBlur={handleBlur}
211
+ onKeyDown={handleKeyDown}
157
212
  placeholder={t('task_name_placeholder')}
158
213
  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"
159
214
  autoFocus
@@ -30,6 +30,8 @@ export interface UseTaskFormResetProps {
30
30
  isCreateMode: boolean;
31
31
  task?: Task;
32
32
  filters?: TaskFilters;
33
+ taskHydrationVersion?: number;
34
+ preserveNameOnHydration?: boolean;
33
35
 
34
36
  // State setters - using React dispatch types for compatibility
35
37
  setName: React.Dispatch<React.SetStateAction<string>>;
@@ -55,6 +57,8 @@ export function useTaskFormReset({
55
57
  isCreateMode,
56
58
  task,
57
59
  filters,
60
+ taskHydrationVersion = 0,
61
+ preserveNameOnHydration = false,
58
62
  setName,
59
63
  setDescription,
60
64
  setPriority,
@@ -67,12 +71,15 @@ export function useTaskFormReset({
67
71
  setSelectedProjects,
68
72
  }: UseTaskFormResetProps): void {
69
73
  const previousTaskIdRef = useRef<string | null>(null);
74
+ const previousTaskHydrationVersionRef = useRef<number>(taskHydrationVersion);
70
75
  const previousIsOpenRef = useRef<boolean>(false);
71
76
  const isMountedRef = useRef(true);
72
77
 
73
78
  // Reset form when task changes or dialog opens
74
79
  useEffect(() => {
75
80
  const taskIdChanged = previousTaskIdRef.current !== task?.id;
81
+ const taskHydrationVersionChanged =
82
+ previousTaskHydrationVersionRef.current !== taskHydrationVersion;
76
83
  const justOpened = isOpen && !previousIsOpenRef.current;
77
84
  previousIsOpenRef.current = isOpen;
78
85
 
@@ -88,8 +95,14 @@ export function useTaskFormReset({
88
95
  // In edit mode, reset whenever the dialog opens or the task changes.
89
96
  // We don't reset on close (see below) so we must reset on every open
90
97
  // to ensure the form reflects the latest DB state, even for the same task.
91
- if (isOpen && !isCreateMode && (taskIdChanged || justOpened)) {
92
- setName(task?.name || '');
98
+ if (
99
+ isOpen &&
100
+ !isCreateMode &&
101
+ (taskIdChanged || taskHydrationVersionChanged || justOpened)
102
+ ) {
103
+ if (!(taskHydrationVersionChanged && preserveNameOnHydration)) {
104
+ setName(task?.name || '');
105
+ }
93
106
  setDescription(getDescriptionContent(task?.description));
94
107
  setPriority(task?.priority || null);
95
108
  setStartDate(task?.start_date ? new Date(task?.start_date) : undefined);
@@ -100,6 +113,7 @@ export function useTaskFormReset({
100
113
  setSelectedAssignees(task?.assignees || []);
101
114
  setSelectedProjects(task?.projects || []);
102
115
  if (task?.id) previousTaskIdRef.current = task.id;
116
+ previousTaskHydrationVersionRef.current = taskHydrationVersion;
103
117
  } else if (
104
118
  isOpen &&
105
119
  (isCreateMode || task?.id === 'new') &&
@@ -122,6 +136,8 @@ export function useTaskFormReset({
122
136
  isCreateMode,
123
137
  isOpen,
124
138
  task,
139
+ taskHydrationVersion,
140
+ preserveNameOnHydration,
125
141
  filters,
126
142
  setName,
127
143
  setDescription,
@@ -66,7 +66,6 @@ export function useTaskRealtimeSync({
66
66
  isCreateMode,
67
67
  isOpen,
68
68
  realtimeEnabled = true,
69
- isPersonalWorkspace = false,
70
69
  name,
71
70
  priority,
72
71
  startDate,
@@ -262,7 +261,7 @@ export function useTaskRealtimeSync({
262
261
  );
263
262
 
264
263
  const { broadcast } = useBoardRealtime(boardId, {
265
- enabled: realtimeActive && !isPersonalWorkspace,
264
+ enabled: realtimeActive,
266
265
  onTaskChange: handleTaskChange,
267
266
  onTaskRelationsChange: handleTaskRelationsChange,
268
267
  });
@@ -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) {
@@ -47,6 +47,7 @@ interface TaskDialogActionsProps {
47
47
  onNavigateBack?: () => void;
48
48
  onOpenShareDialog?: () => void;
49
49
  disabled?: boolean;
50
+ controlsDisabled?: boolean;
50
51
  }
51
52
 
52
53
  export function TaskDialogActions({
@@ -64,6 +65,7 @@ export function TaskDialogActions({
64
65
  onNavigateBack,
65
66
  onOpenShareDialog,
66
67
  disabled = false,
68
+ controlsDisabled = false,
67
69
  }: TaskDialogActionsProps) {
68
70
  const t = useTranslations();
69
71
  const tasksHref = useTasksHref();
@@ -75,7 +77,7 @@ export function TaskDialogActions({
75
77
  return (
76
78
  <>
77
79
  {/* Share button - only in edit mode */}
78
- {!isCreateMode && taskId && onOpenShareDialog && (
80
+ {!isCreateMode && taskId && onOpenShareDialog && !controlsDisabled && (
79
81
  <Tooltip>
80
82
  <TooltipTrigger asChild>
81
83
  <Button
@@ -95,7 +97,7 @@ export function TaskDialogActions({
95
97
  )}
96
98
 
97
99
  {/* More options menu - only in edit mode */}
98
- {!isCreateMode && taskId && !disabled && (
100
+ {!isCreateMode && taskId && !disabled && !controlsDisabled && (
99
101
  <DropdownMenu open={isMoreMenuOpen} onOpenChange={setIsMoreMenuOpen}>
100
102
  <DropdownMenuTrigger asChild>
101
103
  <Button
@@ -164,7 +166,7 @@ export function TaskDialogActions({
164
166
  )}
165
167
 
166
168
  {/* Back to related task button - only in create mode with pending relationship */}
167
- {showBackButton && (
169
+ {showBackButton && !controlsDisabled && (
168
170
  <Tooltip>
169
171
  <TooltipTrigger asChild>
170
172
  <Button