@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
|
@@ -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<
|
|
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={
|
|
58
|
-
|
|
59
|
-
|
|
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 (
|
|
92
|
-
|
|
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
|
|
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 {
|
|
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
|