@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.
- package/CHANGELOG.md +14 -0
- package/package.json +5 -5
- package/src/components/ui/custom/settings/task-settings.tsx +76 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +126 -0
- 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/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +172 -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-panel.tsx +196 -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 +277 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +189 -0
- package/src/components/ui/finance/wallets/query-invalidation.ts +2 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +7 -3
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +10 -0
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +7 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +21 -5
- 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/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +10 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +141 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +208 -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/task-list-selector.tsx +36 -20
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +24 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +18 -3
- 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-properties-section.tsx +300 -172
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +411 -323
- 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/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx
CHANGED
|
@@ -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
|
-
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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=
|
|
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 {
|
|
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) {
|