@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
package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AlertCircle,
|
|
5
|
+
Calendar,
|
|
6
|
+
CheckCircle2,
|
|
7
|
+
Clock,
|
|
8
|
+
Flag,
|
|
9
|
+
FolderKanban,
|
|
10
|
+
ListTodo,
|
|
11
|
+
Loader2,
|
|
12
|
+
Sparkles,
|
|
13
|
+
Tag,
|
|
14
|
+
Timer,
|
|
15
|
+
X,
|
|
16
|
+
} from '@tuturuuu/icons';
|
|
17
|
+
import type { WorkspaceTaskSuggestionTask } from '@tuturuuu/internal-api/tasks';
|
|
18
|
+
import { Badge } from '@tuturuuu/ui/badge';
|
|
19
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
20
|
+
import { Checkbox } from '@tuturuuu/ui/checkbox';
|
|
21
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
|
|
22
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
23
|
+
import dayjs from 'dayjs';
|
|
24
|
+
import { useTranslations } from 'next-intl';
|
|
25
|
+
|
|
26
|
+
interface SmartTaskSuggestionsButtonProps {
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
isLoading?: boolean;
|
|
29
|
+
onClick: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SmartTaskSuggestionsPanelProps {
|
|
33
|
+
suggestions: WorkspaceTaskSuggestionTask[];
|
|
34
|
+
selectedSuggestionIds: string[];
|
|
35
|
+
createErrors?: Record<string, string>;
|
|
36
|
+
creatingSuggestionIds?: string[];
|
|
37
|
+
errorMessage?: string | null;
|
|
38
|
+
isCreatingSelected?: boolean;
|
|
39
|
+
isLoading?: boolean;
|
|
40
|
+
onApplyFirst: () => void;
|
|
41
|
+
onApplySuggestion: (suggestion: WorkspaceTaskSuggestionTask) => void;
|
|
42
|
+
onClose: () => void;
|
|
43
|
+
onCreateSelected: () => void;
|
|
44
|
+
onRetry: () => void;
|
|
45
|
+
onToggleSuggestion: (suggestionId: string) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatDuration(minutes: number) {
|
|
49
|
+
if (minutes < 60) {
|
|
50
|
+
return `${minutes}m`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const hours = Math.floor(minutes / 60);
|
|
54
|
+
const remainder = minutes % 60;
|
|
55
|
+
|
|
56
|
+
return remainder ? `${hours}h ${remainder}m` : `${hours}h`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function SuggestionChip({
|
|
60
|
+
children,
|
|
61
|
+
icon,
|
|
62
|
+
}: {
|
|
63
|
+
children: React.ReactNode;
|
|
64
|
+
icon: React.ReactNode;
|
|
65
|
+
}) {
|
|
66
|
+
return (
|
|
67
|
+
<Badge
|
|
68
|
+
variant="secondary"
|
|
69
|
+
className="h-6 gap-1 rounded-md px-1.5 font-normal text-xs"
|
|
70
|
+
>
|
|
71
|
+
{icon}
|
|
72
|
+
<span className="max-w-28 truncate">{children}</span>
|
|
73
|
+
</Badge>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function SuggestionCard({
|
|
78
|
+
creating,
|
|
79
|
+
error,
|
|
80
|
+
multi,
|
|
81
|
+
onApply,
|
|
82
|
+
onToggle,
|
|
83
|
+
selected,
|
|
84
|
+
suggestion,
|
|
85
|
+
}: {
|
|
86
|
+
creating?: boolean;
|
|
87
|
+
error?: string;
|
|
88
|
+
multi: boolean;
|
|
89
|
+
onApply: () => void;
|
|
90
|
+
onToggle: () => void;
|
|
91
|
+
selected: boolean;
|
|
92
|
+
suggestion: WorkspaceTaskSuggestionTask;
|
|
93
|
+
}) {
|
|
94
|
+
const t = useTranslations('ws-task-boards.dialog');
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
className={cn(
|
|
99
|
+
'rounded-lg border bg-background p-3 transition-colors',
|
|
100
|
+
selected && 'border-dynamic-blue/60 bg-dynamic-blue/5'
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
<div className="flex items-start gap-2">
|
|
104
|
+
{multi && (
|
|
105
|
+
<Checkbox
|
|
106
|
+
aria-label={t('smart_select_suggestion')}
|
|
107
|
+
checked={selected}
|
|
108
|
+
className="mt-0.5"
|
|
109
|
+
onCheckedChange={onToggle}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
113
|
+
<div className="flex items-start justify-between gap-2">
|
|
114
|
+
<div className="min-w-0">
|
|
115
|
+
<div className="truncate font-medium text-sm">
|
|
116
|
+
{suggestion.title}
|
|
117
|
+
</div>
|
|
118
|
+
{suggestion.description && (
|
|
119
|
+
<div className="line-clamp-2 text-muted-foreground text-xs">
|
|
120
|
+
{suggestion.description}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
{!multi && (
|
|
125
|
+
<Button size="sm" type="button" onClick={onApply}>
|
|
126
|
+
<Sparkles className="h-4 w-4" />
|
|
127
|
+
{t('smart_apply_suggestion')}
|
|
128
|
+
</Button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="flex flex-wrap gap-1.5">
|
|
133
|
+
{suggestion.listName && (
|
|
134
|
+
<SuggestionChip icon={<ListTodo className="h-3 w-3" />}>
|
|
135
|
+
{suggestion.listName}
|
|
136
|
+
</SuggestionChip>
|
|
137
|
+
)}
|
|
138
|
+
{suggestion.priority && (
|
|
139
|
+
<SuggestionChip icon={<Flag className="h-3 w-3" />}>
|
|
140
|
+
{t(`priority.${suggestion.priority}`)}
|
|
141
|
+
</SuggestionChip>
|
|
142
|
+
)}
|
|
143
|
+
{suggestion.endDate && (
|
|
144
|
+
<SuggestionChip icon={<Calendar className="h-3 w-3" />}>
|
|
145
|
+
{dayjs(suggestion.endDate).format('MMM D')}
|
|
146
|
+
</SuggestionChip>
|
|
147
|
+
)}
|
|
148
|
+
{suggestion.durationMinutes && (
|
|
149
|
+
<SuggestionChip icon={<Clock className="h-3 w-3" />}>
|
|
150
|
+
{formatDuration(suggestion.durationMinutes)}
|
|
151
|
+
</SuggestionChip>
|
|
152
|
+
)}
|
|
153
|
+
{suggestion.estimationPoints != null && (
|
|
154
|
+
<SuggestionChip icon={<Timer className="h-3 w-3" />}>
|
|
155
|
+
{suggestion.estimationPoints}
|
|
156
|
+
</SuggestionChip>
|
|
157
|
+
)}
|
|
158
|
+
{suggestion.labels.map((label) => (
|
|
159
|
+
<SuggestionChip key={label.id} icon={<Tag className="h-3 w-3" />}>
|
|
160
|
+
{label.name}
|
|
161
|
+
</SuggestionChip>
|
|
162
|
+
))}
|
|
163
|
+
{suggestion.projects.map((project) => (
|
|
164
|
+
<SuggestionChip
|
|
165
|
+
key={project.id}
|
|
166
|
+
icon={<FolderKanban className="h-3 w-3" />}
|
|
167
|
+
>
|
|
168
|
+
{project.name}
|
|
169
|
+
</SuggestionChip>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{suggestion.reason && (
|
|
174
|
+
<div className="text-muted-foreground text-xs">
|
|
175
|
+
{suggestion.reason}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{error && (
|
|
180
|
+
<div className="flex items-center gap-1.5 text-destructive text-xs">
|
|
181
|
+
<AlertCircle className="h-3.5 w-3.5" />
|
|
182
|
+
{error}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{creating && (
|
|
187
|
+
<div className="flex items-center gap-1.5 text-muted-foreground text-xs">
|
|
188
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
189
|
+
{t('smart_creating_task')}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function SmartTaskSuggestionsButton({
|
|
199
|
+
disabled,
|
|
200
|
+
isLoading,
|
|
201
|
+
onClick,
|
|
202
|
+
}: SmartTaskSuggestionsButtonProps) {
|
|
203
|
+
const t = useTranslations('ws-task-boards.dialog');
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<Tooltip>
|
|
207
|
+
<TooltipTrigger asChild>
|
|
208
|
+
<Button
|
|
209
|
+
type="button"
|
|
210
|
+
variant="ghost"
|
|
211
|
+
size="icon"
|
|
212
|
+
aria-label={t('smart_suggest')}
|
|
213
|
+
disabled={disabled || isLoading}
|
|
214
|
+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
215
|
+
onClick={onClick}
|
|
216
|
+
>
|
|
217
|
+
{isLoading ? (
|
|
218
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
219
|
+
) : (
|
|
220
|
+
<Sparkles className="h-4 w-4" />
|
|
221
|
+
)}
|
|
222
|
+
</Button>
|
|
223
|
+
</TooltipTrigger>
|
|
224
|
+
<TooltipContent side="bottom">{t('smart_suggest')}</TooltipContent>
|
|
225
|
+
</Tooltip>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function SmartTaskSuggestionsPanel({
|
|
230
|
+
suggestions,
|
|
231
|
+
selectedSuggestionIds,
|
|
232
|
+
createErrors = {},
|
|
233
|
+
creatingSuggestionIds = [],
|
|
234
|
+
errorMessage,
|
|
235
|
+
isCreatingSelected,
|
|
236
|
+
isLoading,
|
|
237
|
+
onApplyFirst,
|
|
238
|
+
onApplySuggestion,
|
|
239
|
+
onClose,
|
|
240
|
+
onCreateSelected,
|
|
241
|
+
onRetry,
|
|
242
|
+
onToggleSuggestion,
|
|
243
|
+
}: SmartTaskSuggestionsPanelProps) {
|
|
244
|
+
const t = useTranslations('ws-task-boards.dialog');
|
|
245
|
+
const selectedCount = selectedSuggestionIds.length;
|
|
246
|
+
|
|
247
|
+
if (isLoading) {
|
|
248
|
+
return (
|
|
249
|
+
<div className="rounded-lg border bg-muted/30 p-3">
|
|
250
|
+
<div className="flex items-center gap-2 text-sm">
|
|
251
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
252
|
+
<span>{t('smart_generating')}</span>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (errorMessage) {
|
|
259
|
+
return (
|
|
260
|
+
<div className="rounded-lg border border-destructive/40 bg-destructive/5 p-3">
|
|
261
|
+
<div className="flex items-start justify-between gap-2">
|
|
262
|
+
<div className="flex min-w-0 gap-2">
|
|
263
|
+
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
|
264
|
+
<div className="min-w-0 space-y-1">
|
|
265
|
+
<div className="font-medium text-sm">
|
|
266
|
+
{t('smart_suggestions_failed')}
|
|
267
|
+
</div>
|
|
268
|
+
<div className="text-muted-foreground text-xs">
|
|
269
|
+
{errorMessage}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
<Button size="sm" type="button" variant="ghost" onClick={onRetry}>
|
|
274
|
+
{t('retry')}
|
|
275
|
+
</Button>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!suggestions.length) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const multi = suggestions.length > 1;
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<div className="rounded-lg border bg-muted/20 p-3">
|
|
289
|
+
<div className="mb-3 flex items-start justify-between gap-2">
|
|
290
|
+
<div className="min-w-0">
|
|
291
|
+
<div className="flex items-center gap-1.5 font-medium text-sm">
|
|
292
|
+
<Sparkles className="h-4 w-4" />
|
|
293
|
+
{multi
|
|
294
|
+
? t('smart_multiple_suggestions')
|
|
295
|
+
: t('smart_one_suggestion')}
|
|
296
|
+
</div>
|
|
297
|
+
<div className="text-muted-foreground text-xs">
|
|
298
|
+
{multi
|
|
299
|
+
? t('smart_multiple_suggestions_description')
|
|
300
|
+
: t('smart_one_suggestion_description')}
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
<Button
|
|
304
|
+
type="button"
|
|
305
|
+
variant="ghost"
|
|
306
|
+
size="icon"
|
|
307
|
+
aria-label={t('smart_dismiss')}
|
|
308
|
+
className="h-7 w-7 shrink-0"
|
|
309
|
+
onClick={onClose}
|
|
310
|
+
>
|
|
311
|
+
<X className="h-3.5 w-3.5" />
|
|
312
|
+
</Button>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<div className="space-y-2">
|
|
316
|
+
{suggestions.map((suggestion) => (
|
|
317
|
+
<SuggestionCard
|
|
318
|
+
key={suggestion.id}
|
|
319
|
+
creating={creatingSuggestionIds.includes(suggestion.id)}
|
|
320
|
+
error={createErrors[suggestion.id]}
|
|
321
|
+
multi={multi}
|
|
322
|
+
selected={selectedSuggestionIds.includes(suggestion.id)}
|
|
323
|
+
suggestion={suggestion}
|
|
324
|
+
onApply={() => onApplySuggestion(suggestion)}
|
|
325
|
+
onToggle={() => onToggleSuggestion(suggestion.id)}
|
|
326
|
+
/>
|
|
327
|
+
))}
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
{multi && (
|
|
331
|
+
<div className="mt-3 flex flex-wrap items-center justify-between gap-2">
|
|
332
|
+
<Button
|
|
333
|
+
type="button"
|
|
334
|
+
size="sm"
|
|
335
|
+
variant="secondary"
|
|
336
|
+
onClick={onApplyFirst}
|
|
337
|
+
>
|
|
338
|
+
<CheckCircle2 className="h-4 w-4" />
|
|
339
|
+
{t('smart_apply_first')}
|
|
340
|
+
</Button>
|
|
341
|
+
<Button
|
|
342
|
+
type="button"
|
|
343
|
+
size="sm"
|
|
344
|
+
disabled={!selectedCount || isCreatingSelected}
|
|
345
|
+
onClick={onCreateSelected}
|
|
346
|
+
>
|
|
347
|
+
{isCreatingSelected ? (
|
|
348
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
349
|
+
) : (
|
|
350
|
+
<Sparkles className="h-4 w-4" />
|
|
351
|
+
)}
|
|
352
|
+
{t('smart_create_selected', { count: selectedCount })}
|
|
353
|
+
</Button>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|
package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx
CHANGED
|
@@ -36,7 +36,7 @@ export interface TaskDescriptionEditorProps {
|
|
|
36
36
|
// Refs
|
|
37
37
|
editorRef: React.RefObject<HTMLDivElement | null>;
|
|
38
38
|
richTextEditorRef: React.RefObject<HTMLDivElement | null>;
|
|
39
|
-
titleInputRef: React.RefObject<HTMLInputElement | null>;
|
|
39
|
+
titleInputRef: React.RefObject<HTMLInputElement | HTMLTextAreaElement | null>;
|
|
40
40
|
lastCursorPositionRef: React.RefObject<number | null>;
|
|
41
41
|
targetEditorCursorRef: React.RefObject<number | null>;
|
|
42
42
|
flushEditorPendingRef: React.RefObject<
|
|
@@ -222,6 +222,8 @@ interface TaskDialogHeaderProps {
|
|
|
222
222
|
onOpenShareDialog?: () => void;
|
|
223
223
|
/** Whether the dialog is in read-only mode */
|
|
224
224
|
disabled?: boolean;
|
|
225
|
+
/** Whether task-dependent actions should be disabled while the task hydrates. */
|
|
226
|
+
controlsDisabled?: boolean;
|
|
225
227
|
/** Callback to scroll the editor to a collaborator's cursor position */
|
|
226
228
|
onScrollToUserCursor?: (userId: string, displayName: string) => void;
|
|
227
229
|
}
|
|
@@ -262,6 +264,7 @@ export function TaskDialogHeader({
|
|
|
262
264
|
isPersonalWorkspace = false,
|
|
263
265
|
onOpenShareDialog,
|
|
264
266
|
disabled = false,
|
|
267
|
+
controlsDisabled = false,
|
|
265
268
|
onScrollToUserCursor,
|
|
266
269
|
}: TaskDialogHeaderProps) {
|
|
267
270
|
const t = useTranslations();
|
|
@@ -448,7 +451,7 @@ export function TaskDialogHeader({
|
|
|
448
451
|
)}
|
|
449
452
|
|
|
450
453
|
{/* Quick Settings */}
|
|
451
|
-
{!
|
|
454
|
+
{!controlsDisabled && (
|
|
452
455
|
<QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
|
|
453
456
|
)}
|
|
454
457
|
|
|
@@ -467,10 +470,11 @@ export function TaskDialogHeader({
|
|
|
467
470
|
onNavigateBack={onNavigateBack}
|
|
468
471
|
onOpenShareDialog={onOpenShareDialog}
|
|
469
472
|
disabled={disabled}
|
|
473
|
+
controlsDisabled={controlsDisabled}
|
|
470
474
|
/>
|
|
471
475
|
|
|
472
476
|
{/* Hide save button in edit mode when realtime is enabled (either cursors or Yjs sync) */}
|
|
473
|
-
{!
|
|
477
|
+
{!controlsDisabled &&
|
|
474
478
|
(isCreateMode || (!collaborationMode && !realtimeEnabled)) && (
|
|
475
479
|
<Tooltip>
|
|
476
480
|
<TooltipTrigger asChild>
|
|
@@ -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');
|
|
@@ -23,7 +27,9 @@ function renderTaskNameInput({
|
|
|
23
27
|
editorElement.tabIndex = -1;
|
|
24
28
|
editorWrapper.append(editorElement);
|
|
25
29
|
|
|
26
|
-
const titleInputRef = {
|
|
30
|
+
const titleInputRef = {
|
|
31
|
+
current: null as HTMLInputElement | HTMLTextAreaElement | null,
|
|
32
|
+
};
|
|
27
33
|
const editorRef = { current: editorWrapper };
|
|
28
34
|
const lastCursorPositionRef = { current: null };
|
|
29
35
|
const targetEditorCursorRef = { current: targetCursor };
|
|
@@ -37,6 +43,8 @@ function renderTaskNameInput({
|
|
|
37
43
|
setName: vi.fn(),
|
|
38
44
|
updateName: vi.fn(),
|
|
39
45
|
flushNameUpdate: vi.fn(),
|
|
46
|
+
variant,
|
|
47
|
+
onSubmit,
|
|
40
48
|
};
|
|
41
49
|
|
|
42
50
|
render(<TaskNameInput {...props} />);
|
|
@@ -60,6 +68,18 @@ describe('TaskNameInput', () => {
|
|
|
60
68
|
vi.restoreAllMocks();
|
|
61
69
|
});
|
|
62
70
|
|
|
71
|
+
it.each([
|
|
72
|
+
'fullscreen',
|
|
73
|
+
'compact',
|
|
74
|
+
] as const)('places the initial %s title caret at the end', (variant) => {
|
|
75
|
+
const { input } = renderTaskNameInput({ variant });
|
|
76
|
+
const expectedCursorPosition = 'APIs for Agent Pi'.length;
|
|
77
|
+
|
|
78
|
+
expect(input).toHaveFocus();
|
|
79
|
+
expect(input).toHaveProperty('selectionStart', expectedCursorPosition);
|
|
80
|
+
expect(input).toHaveProperty('selectionEnd', expectedCursorPosition);
|
|
81
|
+
});
|
|
82
|
+
|
|
63
83
|
it('defers focusing the description editor after Enter', () => {
|
|
64
84
|
const { focusEditor, targetEditorCursorRef, input } = renderTaskNameInput();
|
|
65
85
|
|
|
@@ -137,4 +157,24 @@ describe('TaskNameInput', () => {
|
|
|
137
157
|
window.removeEventListener('keydown', windowKeyDown);
|
|
138
158
|
}
|
|
139
159
|
});
|
|
160
|
+
|
|
161
|
+
it('submits from compact mode without focusing the description editor', () => {
|
|
162
|
+
const onSubmit = vi.fn();
|
|
163
|
+
const { focusEditor, targetEditorCursorRef, input } = renderTaskNameInput({
|
|
164
|
+
onSubmit,
|
|
165
|
+
variant: 'compact',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const eventAllowed = fireEvent.keyDown(input, { key: 'Enter' });
|
|
169
|
+
|
|
170
|
+
expect(input.tagName).toBe('TEXTAREA');
|
|
171
|
+
expect(eventAllowed).toBe(false);
|
|
172
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
173
|
+
expect(targetEditorCursorRef.current).toBe(12);
|
|
174
|
+
expect(input).toHaveProperty('value', 'APIs for Agent Pi');
|
|
175
|
+
|
|
176
|
+
vi.runOnlyPendingTimers();
|
|
177
|
+
|
|
178
|
+
expect(focusEditor).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
140
180
|
});
|