@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
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
} from '@tuturuuu/internal-api';
|
|
11
11
|
import type { TaskWithRelations } from '@tuturuuu/types';
|
|
12
12
|
import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
|
|
13
|
+
import { getActiveTaskUserBroadcast } from '@tuturuuu/ui/hooks/useTaskUserRealtime';
|
|
13
14
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
14
15
|
import { useTranslations } from 'next-intl';
|
|
15
16
|
import { useCallback, useState } from 'react';
|
|
17
|
+
import { dispatchTaskSoundCue } from '../shared/task-sound-effects';
|
|
16
18
|
import {
|
|
17
19
|
MY_COMPLETED_TASKS_QUERY_KEY,
|
|
18
20
|
MY_TASKS_QUERY_KEY,
|
|
@@ -37,6 +39,7 @@ export function useTaskContextActions({
|
|
|
37
39
|
const [isLoading, setIsLoading] = useState(false);
|
|
38
40
|
const taskWorkspaceId = task.list?.board?.ws_id ?? null;
|
|
39
41
|
const taskBoardId = task.list?.board?.id ?? null;
|
|
42
|
+
const taskListId = task.list_id ?? task.list?.id ?? null;
|
|
40
43
|
|
|
41
44
|
const invalidateQueries = useCallback(() => {
|
|
42
45
|
queryClient.invalidateQueries({ queryKey: [MY_TASKS_QUERY_KEY] });
|
|
@@ -65,6 +68,63 @@ export function useTaskContextActions({
|
|
|
65
68
|
[queryClient, task.id]
|
|
66
69
|
);
|
|
67
70
|
|
|
71
|
+
const broadcastTaskUpsert = useCallback(
|
|
72
|
+
(updates: Record<string, unknown>) => {
|
|
73
|
+
const broadcast = getActiveTaskUserBroadcast();
|
|
74
|
+
if (!broadcast) return;
|
|
75
|
+
|
|
76
|
+
const nextListId =
|
|
77
|
+
typeof updates.list_id === 'string' ? updates.list_id : taskListId;
|
|
78
|
+
broadcast('task:upsert', {
|
|
79
|
+
actor_user_id: userId,
|
|
80
|
+
actorUserId: userId,
|
|
81
|
+
boardId: taskBoardId,
|
|
82
|
+
listId: nextListId,
|
|
83
|
+
task: {
|
|
84
|
+
id: task.id,
|
|
85
|
+
name: task.name,
|
|
86
|
+
description: task.description ?? null,
|
|
87
|
+
priority: task.priority ?? null,
|
|
88
|
+
start_date: task.start_date ?? null,
|
|
89
|
+
end_date: task.end_date ?? null,
|
|
90
|
+
list_id: nextListId,
|
|
91
|
+
created_at: task.created_at ?? null,
|
|
92
|
+
list: task.list,
|
|
93
|
+
overrides: task.overrides,
|
|
94
|
+
...updates,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
[task, taskBoardId, taskListId, userId]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const broadcastTaskDelete = useCallback(() => {
|
|
102
|
+
const broadcast = getActiveTaskUserBroadcast();
|
|
103
|
+
if (!broadcast) return;
|
|
104
|
+
|
|
105
|
+
broadcast('task:delete', {
|
|
106
|
+
actor_user_id: userId,
|
|
107
|
+
actorUserId: userId,
|
|
108
|
+
boardId: taskBoardId,
|
|
109
|
+
listId: taskListId,
|
|
110
|
+
taskId: task.id,
|
|
111
|
+
});
|
|
112
|
+
}, [task.id, taskBoardId, taskListId, userId]);
|
|
113
|
+
|
|
114
|
+
const broadcastTaskRelationsChanged = useCallback(() => {
|
|
115
|
+
const broadcast = getActiveTaskUserBroadcast();
|
|
116
|
+
if (!broadcast) return;
|
|
117
|
+
|
|
118
|
+
broadcast('task:relations-changed', {
|
|
119
|
+
actor_user_id: userId,
|
|
120
|
+
actorUserId: userId,
|
|
121
|
+
boardId: taskBoardId,
|
|
122
|
+
listId: taskListId,
|
|
123
|
+
taskId: task.id,
|
|
124
|
+
taskIds: [task.id],
|
|
125
|
+
});
|
|
126
|
+
}, [task.id, taskBoardId, taskListId, userId]);
|
|
127
|
+
|
|
68
128
|
const handlePriorityChange = useCallback(
|
|
69
129
|
async (priority: TaskPriority | null) => {
|
|
70
130
|
setIsLoading(true);
|
|
@@ -72,7 +132,9 @@ export function useTaskContextActions({
|
|
|
72
132
|
try {
|
|
73
133
|
if (!taskWorkspaceId) throw new Error('Task workspace not found');
|
|
74
134
|
await updateWorkspaceTask(taskWorkspaceId, task.id, { priority });
|
|
135
|
+
broadcastTaskUpsert({ priority });
|
|
75
136
|
invalidateQueries();
|
|
137
|
+
dispatchTaskSoundCue('update');
|
|
76
138
|
} catch {
|
|
77
139
|
toast.error(t('failed_to_update'));
|
|
78
140
|
invalidateQueries();
|
|
@@ -80,7 +142,14 @@ export function useTaskContextActions({
|
|
|
80
142
|
setIsLoading(false);
|
|
81
143
|
}
|
|
82
144
|
},
|
|
83
|
-
[
|
|
145
|
+
[
|
|
146
|
+
task.id,
|
|
147
|
+
taskWorkspaceId,
|
|
148
|
+
updateTaskInCache,
|
|
149
|
+
invalidateQueries,
|
|
150
|
+
broadcastTaskUpsert,
|
|
151
|
+
t,
|
|
152
|
+
]
|
|
84
153
|
);
|
|
85
154
|
|
|
86
155
|
const handleDueDateChange = useCallback(
|
|
@@ -96,7 +165,9 @@ export function useTaskContextActions({
|
|
|
96
165
|
await updateWorkspaceTask(taskWorkspaceId, task.id, {
|
|
97
166
|
end_date: newDate,
|
|
98
167
|
});
|
|
168
|
+
broadcastTaskUpsert({ end_date: newDate });
|
|
99
169
|
invalidateQueries();
|
|
170
|
+
dispatchTaskSoundCue('update');
|
|
100
171
|
} catch {
|
|
101
172
|
toast.error(t('failed_to_update'));
|
|
102
173
|
invalidateQueries();
|
|
@@ -104,7 +175,14 @@ export function useTaskContextActions({
|
|
|
104
175
|
setIsLoading(false);
|
|
105
176
|
}
|
|
106
177
|
},
|
|
107
|
-
[
|
|
178
|
+
[
|
|
179
|
+
task.id,
|
|
180
|
+
taskWorkspaceId,
|
|
181
|
+
updateTaskInCache,
|
|
182
|
+
invalidateQueries,
|
|
183
|
+
broadcastTaskUpsert,
|
|
184
|
+
t,
|
|
185
|
+
]
|
|
108
186
|
);
|
|
109
187
|
|
|
110
188
|
const handleToggleLabel = useCallback(
|
|
@@ -118,7 +196,9 @@ export function useTaskContextActions({
|
|
|
118
196
|
} else {
|
|
119
197
|
await addWorkspaceTaskLabel(taskWorkspaceId, task.id, labelId);
|
|
120
198
|
}
|
|
199
|
+
broadcastTaskRelationsChanged();
|
|
121
200
|
invalidateQueries();
|
|
201
|
+
dispatchTaskSoundCue('update');
|
|
122
202
|
} catch {
|
|
123
203
|
toast.error(t('failed_to_update'));
|
|
124
204
|
invalidateQueries();
|
|
@@ -126,7 +206,14 @@ export function useTaskContextActions({
|
|
|
126
206
|
setIsLoading(false);
|
|
127
207
|
}
|
|
128
208
|
},
|
|
129
|
-
[
|
|
209
|
+
[
|
|
210
|
+
task.id,
|
|
211
|
+
task.labels,
|
|
212
|
+
taskWorkspaceId,
|
|
213
|
+
invalidateQueries,
|
|
214
|
+
broadcastTaskRelationsChanged,
|
|
215
|
+
t,
|
|
216
|
+
]
|
|
130
217
|
);
|
|
131
218
|
|
|
132
219
|
const handleComplete = useCallback(async () => {
|
|
@@ -147,6 +234,9 @@ export function useTaskContextActions({
|
|
|
147
234
|
await updateWorkspaceTask(taskWorkspaceId, task.id, {
|
|
148
235
|
list_id: doneList.id,
|
|
149
236
|
});
|
|
237
|
+
broadcastTaskUpsert({
|
|
238
|
+
list_id: doneList.id,
|
|
239
|
+
});
|
|
150
240
|
|
|
151
241
|
// Clear redundant personal overrides when task is actually done
|
|
152
242
|
if (
|
|
@@ -167,6 +257,7 @@ export function useTaskContextActions({
|
|
|
167
257
|
|
|
168
258
|
onTaskUpdate();
|
|
169
259
|
onClose();
|
|
260
|
+
dispatchTaskSoundCue('complete');
|
|
170
261
|
} catch {
|
|
171
262
|
toast.error(t('failed_to_update'));
|
|
172
263
|
} finally {
|
|
@@ -177,6 +268,7 @@ export function useTaskContextActions({
|
|
|
177
268
|
taskBoardId,
|
|
178
269
|
taskWorkspaceId,
|
|
179
270
|
task.overrides,
|
|
271
|
+
broadcastTaskUpsert,
|
|
180
272
|
onTaskUpdate,
|
|
181
273
|
onClose,
|
|
182
274
|
t,
|
|
@@ -194,14 +286,16 @@ export function useTaskContextActions({
|
|
|
194
286
|
}
|
|
195
287
|
);
|
|
196
288
|
if (!response.ok) throw new Error('Failed');
|
|
289
|
+
broadcastTaskDelete();
|
|
197
290
|
onTaskUpdate();
|
|
198
291
|
onClose();
|
|
292
|
+
dispatchTaskSoundCue('complete');
|
|
199
293
|
} catch {
|
|
200
294
|
toast.error(t('failed_to_update'));
|
|
201
295
|
} finally {
|
|
202
296
|
setIsLoading(false);
|
|
203
297
|
}
|
|
204
|
-
}, [task.id, onTaskUpdate, onClose, t]);
|
|
298
|
+
}, [task.id, broadcastTaskDelete, onTaskUpdate, onClose, t]);
|
|
205
299
|
|
|
206
300
|
const handleUndoDoneWithMyPart = useCallback(async () => {
|
|
207
301
|
setIsLoading(true);
|
|
@@ -218,14 +312,22 @@ export function useTaskContextActions({
|
|
|
218
312
|
}
|
|
219
313
|
);
|
|
220
314
|
if (!response.ok) throw new Error('Failed');
|
|
315
|
+
broadcastTaskUpsert({
|
|
316
|
+
overrides: {
|
|
317
|
+
...task.overrides,
|
|
318
|
+
personally_unassigned: false,
|
|
319
|
+
completed_at: null,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
221
322
|
onTaskUpdate();
|
|
222
323
|
onClose();
|
|
324
|
+
dispatchTaskSoundCue('update');
|
|
223
325
|
} catch {
|
|
224
326
|
toast.error(t('failed_to_update'));
|
|
225
327
|
} finally {
|
|
226
328
|
setIsLoading(false);
|
|
227
329
|
}
|
|
228
|
-
}, [task.id, onTaskUpdate, onClose, t]);
|
|
330
|
+
}, [task.id, task.overrides, broadcastTaskUpsert, onTaskUpdate, onClose, t]);
|
|
229
331
|
|
|
230
332
|
const handleUndoComplete = useCallback(async () => {
|
|
231
333
|
if (!taskBoardId || !taskWorkspaceId) return;
|
|
@@ -247,15 +349,25 @@ export function useTaskContextActions({
|
|
|
247
349
|
await updateWorkspaceTask(taskWorkspaceId, task.id, {
|
|
248
350
|
list_id: activeList.id,
|
|
249
351
|
});
|
|
352
|
+
broadcastTaskUpsert({ list_id: activeList.id });
|
|
250
353
|
|
|
251
354
|
onTaskUpdate();
|
|
252
355
|
onClose();
|
|
356
|
+
dispatchTaskSoundCue('move');
|
|
253
357
|
} catch {
|
|
254
358
|
toast.error(t('failed_to_update'));
|
|
255
359
|
} finally {
|
|
256
360
|
setIsLoading(false);
|
|
257
361
|
}
|
|
258
|
-
}, [
|
|
362
|
+
}, [
|
|
363
|
+
task.id,
|
|
364
|
+
taskBoardId,
|
|
365
|
+
taskWorkspaceId,
|
|
366
|
+
broadcastTaskUpsert,
|
|
367
|
+
onTaskUpdate,
|
|
368
|
+
onClose,
|
|
369
|
+
t,
|
|
370
|
+
]);
|
|
259
371
|
|
|
260
372
|
const handleUnassignMe = useCallback(async () => {
|
|
261
373
|
setIsLoading(true);
|
|
@@ -283,8 +395,10 @@ export function useTaskContextActions({
|
|
|
283
395
|
Boolean(assigneeId && assigneeId !== userId)
|
|
284
396
|
),
|
|
285
397
|
});
|
|
398
|
+
broadcastTaskDelete();
|
|
286
399
|
onTaskUpdate();
|
|
287
400
|
onClose();
|
|
401
|
+
dispatchTaskSoundCue('update');
|
|
288
402
|
} catch {
|
|
289
403
|
toast.error(t('failed_to_update'));
|
|
290
404
|
invalidateQueries();
|
|
@@ -301,6 +415,7 @@ export function useTaskContextActions({
|
|
|
301
415
|
invalidateQueries,
|
|
302
416
|
t,
|
|
303
417
|
task.assignees,
|
|
418
|
+
broadcastTaskDelete,
|
|
304
419
|
]);
|
|
305
420
|
|
|
306
421
|
const handleDelete = useCallback(async () => {
|
|
@@ -308,14 +423,16 @@ export function useTaskContextActions({
|
|
|
308
423
|
try {
|
|
309
424
|
if (!taskWorkspaceId) throw new Error('Task workspace not found');
|
|
310
425
|
await deleteWorkspaceTask(taskWorkspaceId, task.id);
|
|
426
|
+
broadcastTaskDelete();
|
|
311
427
|
onTaskUpdate();
|
|
312
428
|
onClose();
|
|
429
|
+
dispatchTaskSoundCue('delete');
|
|
313
430
|
} catch {
|
|
314
431
|
toast.error(t('failed_to_update'));
|
|
315
432
|
} finally {
|
|
316
433
|
setIsLoading(false);
|
|
317
434
|
}
|
|
318
|
-
}, [task.id, taskWorkspaceId, onTaskUpdate, onClose, t]);
|
|
435
|
+
}, [task.id, taskWorkspaceId, broadcastTaskDelete, onTaskUpdate, onClose, t]);
|
|
319
436
|
|
|
320
437
|
return {
|
|
321
438
|
isLoading,
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
useTaskDialogContext,
|
|
10
10
|
} from '../task-dialog-provider';
|
|
11
11
|
|
|
12
|
-
const {
|
|
13
|
-
|
|
12
|
+
const { mockGetTaskDialogHydration } = vi.hoisted(() => ({
|
|
13
|
+
mockGetTaskDialogHydration: vi.fn(),
|
|
14
14
|
}));
|
|
15
15
|
|
|
16
16
|
vi.mock('@tuturuuu/internal-api/tasks', async () => {
|
|
@@ -20,7 +20,7 @@ vi.mock('@tuturuuu/internal-api/tasks', async () => {
|
|
|
20
20
|
|
|
21
21
|
return {
|
|
22
22
|
...actual,
|
|
23
|
-
|
|
23
|
+
getTaskDialogHydration: mockGetTaskDialogHydration,
|
|
24
24
|
};
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -59,13 +59,25 @@ const wrapper = ({ children }: { children: ReactNode }) => (
|
|
|
59
59
|
<TaskDialogProvider>{children}</TaskDialogProvider>
|
|
60
60
|
);
|
|
61
61
|
|
|
62
|
+
function createDeferred<T>() {
|
|
63
|
+
let resolve!: (value: T) => void;
|
|
64
|
+
let reject!: (reason?: unknown) => void;
|
|
65
|
+
const promise = new Promise<T>((promiseResolve, promiseReject) => {
|
|
66
|
+
resolve = promiseResolve;
|
|
67
|
+
reject = promiseReject;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return { promise, resolve, reject };
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
describe('TaskDialogProvider', () => {
|
|
63
74
|
afterEach(() => {
|
|
75
|
+
mockGetTaskDialogHydration.mockReset();
|
|
64
76
|
vi.useRealTimers();
|
|
65
77
|
});
|
|
66
78
|
|
|
67
79
|
it('should enable collaborationMode for paid task workspaces opened by id', async () => {
|
|
68
|
-
|
|
80
|
+
mockGetTaskDialogHydration.mockResolvedValueOnce({
|
|
69
81
|
task: {
|
|
70
82
|
...mockTask,
|
|
71
83
|
list: { board_id: 'board-1' },
|
|
@@ -87,7 +99,7 @@ describe('TaskDialogProvider', () => {
|
|
|
87
99
|
});
|
|
88
100
|
|
|
89
101
|
it('should disable collaborationMode for free task workspaces opened by id', async () => {
|
|
90
|
-
|
|
102
|
+
mockGetTaskDialogHydration.mockResolvedValueOnce({
|
|
91
103
|
task: {
|
|
92
104
|
...mockTask,
|
|
93
105
|
list: { board_id: 'board-1' },
|
|
@@ -108,6 +120,206 @@ describe('TaskDialogProvider', () => {
|
|
|
108
120
|
expect(result.current.state.taskWorkspaceTier).toBe('FREE');
|
|
109
121
|
});
|
|
110
122
|
|
|
123
|
+
it('opens by id immediately from an initial snapshot before hydrating task details', async () => {
|
|
124
|
+
const initialSharedContext = {
|
|
125
|
+
boardConfig: {
|
|
126
|
+
id: 'board-1',
|
|
127
|
+
name: 'Visible board',
|
|
128
|
+
ws_id: 'workspace-1',
|
|
129
|
+
ticket_prefix: 'VIS',
|
|
130
|
+
},
|
|
131
|
+
availableLists: [mockList],
|
|
132
|
+
workspaceLabels: [],
|
|
133
|
+
workspaceMembers: [],
|
|
134
|
+
workspaceProjects: [],
|
|
135
|
+
};
|
|
136
|
+
const deferred = createDeferred<{
|
|
137
|
+
task: Task & { list?: { board_id?: string | null } | null };
|
|
138
|
+
availableLists: TaskList[];
|
|
139
|
+
taskWsId: string;
|
|
140
|
+
taskWorkspacePersonal: boolean;
|
|
141
|
+
taskWorkspaceTier: 'PRO';
|
|
142
|
+
}>();
|
|
143
|
+
mockGetTaskDialogHydration.mockReturnValueOnce(deferred.promise);
|
|
144
|
+
|
|
145
|
+
const { result } = renderHook(() => useTaskDialogContext(), { wrapper });
|
|
146
|
+
let openPromise!: Promise<boolean>;
|
|
147
|
+
|
|
148
|
+
act(() => {
|
|
149
|
+
openPromise = result.current.openTaskById(mockTask.id, {
|
|
150
|
+
initialTask: { ...mockTask, name: 'Visible snapshot' },
|
|
151
|
+
boardId: 'board-1',
|
|
152
|
+
availableLists: [mockList],
|
|
153
|
+
taskWsId: 'workspace-1',
|
|
154
|
+
initialSharedContext,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.current.state).toMatchObject({
|
|
159
|
+
isOpen: true,
|
|
160
|
+
isHydratingTask: true,
|
|
161
|
+
taskLoadError: false,
|
|
162
|
+
boardId: 'board-1',
|
|
163
|
+
realtimeEnabled: false,
|
|
164
|
+
initialSharedContext,
|
|
165
|
+
task: {
|
|
166
|
+
id: mockTask.id,
|
|
167
|
+
name: 'Visible snapshot',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await act(async () => {
|
|
172
|
+
deferred.resolve({
|
|
173
|
+
task: {
|
|
174
|
+
...mockTask,
|
|
175
|
+
name: 'Hydrated task',
|
|
176
|
+
list: { board_id: 'board-1' },
|
|
177
|
+
},
|
|
178
|
+
availableLists: [mockList],
|
|
179
|
+
taskWsId: 'workspace-1',
|
|
180
|
+
taskWorkspacePersonal: false,
|
|
181
|
+
taskWorkspaceTier: 'PRO',
|
|
182
|
+
});
|
|
183
|
+
await openPromise;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(mockGetTaskDialogHydration).toHaveBeenCalledWith(
|
|
187
|
+
mockTask.id,
|
|
188
|
+
{
|
|
189
|
+
taskWsId: 'workspace-1',
|
|
190
|
+
taskWorkspacePersonal: undefined,
|
|
191
|
+
taskWorkspaceTier: undefined,
|
|
192
|
+
},
|
|
193
|
+
expect.objectContaining({
|
|
194
|
+
fetch: expect.any(Function),
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
expect(result.current.state).toMatchObject({
|
|
198
|
+
isOpen: true,
|
|
199
|
+
isHydratingTask: false,
|
|
200
|
+
taskLoadError: false,
|
|
201
|
+
collaborationMode: true,
|
|
202
|
+
realtimeEnabled: true,
|
|
203
|
+
taskHydrationVersion: 1,
|
|
204
|
+
taskWorkspaceTier: 'PRO',
|
|
205
|
+
task: {
|
|
206
|
+
id: mockTask.id,
|
|
207
|
+
name: 'Hydrated task',
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
expect(result.current.state.initialSharedContext).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('keeps the dialog open in a non-editable error state when hydration fails', async () => {
|
|
214
|
+
const initialSharedContext = {
|
|
215
|
+
boardConfig: {
|
|
216
|
+
id: 'board-1',
|
|
217
|
+
name: 'Visible board',
|
|
218
|
+
ws_id: 'workspace-1',
|
|
219
|
+
ticket_prefix: 'VIS',
|
|
220
|
+
},
|
|
221
|
+
availableLists: [mockList],
|
|
222
|
+
workspaceLabels: [],
|
|
223
|
+
workspaceMembers: [],
|
|
224
|
+
workspaceProjects: [],
|
|
225
|
+
};
|
|
226
|
+
const deferred = createDeferred<never>();
|
|
227
|
+
mockGetTaskDialogHydration.mockReturnValueOnce(deferred.promise);
|
|
228
|
+
|
|
229
|
+
const { result } = renderHook(() => useTaskDialogContext(), { wrapper });
|
|
230
|
+
let openPromise!: Promise<boolean>;
|
|
231
|
+
|
|
232
|
+
act(() => {
|
|
233
|
+
openPromise = result.current.openTaskById(mockTask.id, {
|
|
234
|
+
initialTask: { ...mockTask, name: 'Visible snapshot' },
|
|
235
|
+
boardId: 'board-1',
|
|
236
|
+
initialSharedContext,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(result.current.state.isOpen).toBe(true);
|
|
241
|
+
expect(result.current.state.isHydratingTask).toBe(true);
|
|
242
|
+
|
|
243
|
+
await act(async () => {
|
|
244
|
+
deferred.reject(new Error('network failed'));
|
|
245
|
+
await openPromise;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(result.current.state).toMatchObject({
|
|
249
|
+
isOpen: true,
|
|
250
|
+
isHydratingTask: false,
|
|
251
|
+
taskLoadError: true,
|
|
252
|
+
initialSharedContext,
|
|
253
|
+
task: {
|
|
254
|
+
id: mockTask.id,
|
|
255
|
+
name: 'Visible snapshot',
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('ignores stale hydration responses after another task opens', async () => {
|
|
261
|
+
const firstDeferred = createDeferred<{
|
|
262
|
+
task: Task & { list?: { board_id?: string | null } | null };
|
|
263
|
+
availableLists: TaskList[];
|
|
264
|
+
taskWsId: string;
|
|
265
|
+
taskWorkspacePersonal: boolean;
|
|
266
|
+
taskWorkspaceTier: 'PRO';
|
|
267
|
+
}>();
|
|
268
|
+
mockGetTaskDialogHydration.mockReturnValueOnce(firstDeferred.promise);
|
|
269
|
+
|
|
270
|
+
const { result } = renderHook(() => useTaskDialogContext(), { wrapper });
|
|
271
|
+
let firstOpenPromise!: Promise<boolean>;
|
|
272
|
+
|
|
273
|
+
act(() => {
|
|
274
|
+
firstOpenPromise = result.current.openTaskById('task-1', {
|
|
275
|
+
initialTask: { ...mockTask, id: 'task-1', name: 'First snapshot' },
|
|
276
|
+
boardId: 'board-1',
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
act(() => {
|
|
280
|
+
result.current.closeDialog();
|
|
281
|
+
});
|
|
282
|
+
act(() => {
|
|
283
|
+
result.current.openTask(
|
|
284
|
+
{ ...mockTask, id: 'task-2', name: 'Second task' },
|
|
285
|
+
'board-1',
|
|
286
|
+
[mockList]
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(result.current.state).toMatchObject({
|
|
291
|
+
isOpen: true,
|
|
292
|
+
task: {
|
|
293
|
+
id: 'task-2',
|
|
294
|
+
name: 'Second task',
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await act(async () => {
|
|
299
|
+
firstDeferred.resolve({
|
|
300
|
+
task: {
|
|
301
|
+
...mockTask,
|
|
302
|
+
id: 'task-1',
|
|
303
|
+
name: 'Stale hydrated task',
|
|
304
|
+
list: { board_id: 'board-1' },
|
|
305
|
+
},
|
|
306
|
+
availableLists: [mockList],
|
|
307
|
+
taskWsId: 'workspace-1',
|
|
308
|
+
taskWorkspacePersonal: false,
|
|
309
|
+
taskWorkspaceTier: 'PRO',
|
|
310
|
+
});
|
|
311
|
+
await firstOpenPromise;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(result.current.state).toMatchObject({
|
|
315
|
+
isOpen: true,
|
|
316
|
+
task: {
|
|
317
|
+
id: 'task-2',
|
|
318
|
+
name: 'Second task',
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
111
323
|
it('should provide initial dialog state', () => {
|
|
112
324
|
const { result } = renderHook(() => useTaskDialogContext(), { wrapper });
|
|
113
325
|
|