@tuturuuu/ui 0.9.0 → 0.11.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 +45 -0
- package/package.json +6 -5
- package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
- package/src/components/ui/custom/nav-link.test.tsx +165 -0
- package/src/components/ui/custom/nav-link.tsx +69 -11
- package/src/components/ui/custom/navigation.tsx +1 -0
- package/src/components/ui/custom/settings/task-settings.tsx +104 -0
- package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
- package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
- package/src/components/ui/custom/settings-dialog-search.ts +75 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
- package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
- package/src/components/ui/custom/workspace-select.tsx +17 -16
- package/src/components/ui/finance/invoices/components/subscription-attendance-summary.tsx +19 -5
- package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.test.tsx +41 -0
- package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.tsx +93 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +82 -70
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +50 -25
- package/src/components/ui/finance/invoices/hooks.ts +11 -2
- package/src/components/ui/finance/invoices/internal-api.ts +5 -0
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +92 -31
- package/src/components/ui/finance/invoices/utils.test.ts +82 -0
- package/src/components/ui/finance/invoices/utils.ts +240 -7
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +16 -0
- package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
- package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
- package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
- package/src/components/ui/tu-do/boards/form.tsx +1 -1
- package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
- package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
- package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
- package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
- package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
- package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +123 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +58 -0
- package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
- package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +16 -4
- package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
- package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
- package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
- package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
- package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +58 -4
- package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.test.ts +97 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.ts +210 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +56 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.tsx +218 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-restore-banner.tsx +83 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.test.tsx +120 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.tsx +180 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/utils.test.ts +100 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/utils.ts +109 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +390 -34
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
- package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
- package/src/components/ui/tu-do/templates/task-template-api.ts +142 -0
- package/src/components/ui/tu-do/templates/task-template-card.tsx +118 -0
- package/src/components/ui/tu-do/templates/task-template-client.tsx +258 -0
- package/src/components/ui/tu-do/templates/task-template-dialogs.test.tsx +167 -0
- package/src/components/ui/tu-do/templates/task-template-dialogs.tsx +376 -0
- package/src/components/ui/tu-do/templates/task-templates-hub.test.tsx +114 -0
- package/src/components/ui/tu-do/templates/task-templates-hub.tsx +50 -0
- package/src/components/ui/tu-do/templates/task-templates-page.tsx +6 -11
- package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
- package/src/hooks/useBoardPresence.ts +364 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
- package/src/lib/workspace-actions.ts +2 -6
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { type QueryClient, useQueryClient } from '@tanstack/react-query';
|
|
2
|
-
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
3
2
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
4
|
-
import { invalidateTaskCaches } from '@tuturuuu/utils/task-helper';
|
|
5
3
|
import {
|
|
6
4
|
type Dispatch,
|
|
7
5
|
type SetStateAction,
|
|
@@ -11,9 +9,11 @@ import {
|
|
|
11
9
|
import { getRandomNewLabelColor } from '../../../utils/taskConstants';
|
|
12
10
|
import {
|
|
13
11
|
type BoardBroadcastFn,
|
|
12
|
+
getActiveBoardRefresh,
|
|
14
13
|
getActiveBroadcast,
|
|
15
14
|
useBoardBroadcast,
|
|
16
15
|
} from '../../board-broadcast-context';
|
|
16
|
+
import { patchTaskInVisibleCaches } from '../../task-cache-patches';
|
|
17
17
|
import type {
|
|
18
18
|
WorkspaceTaskAssignee,
|
|
19
19
|
WorkspaceTaskLabel,
|
|
@@ -129,6 +129,32 @@ function upsertWorkspaceLabelCaches({
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function normalizeTaskAssignees(assignees: WorkspaceTaskAssignee[]) {
|
|
133
|
+
return assignees
|
|
134
|
+
.map((assignee) => {
|
|
135
|
+
const id = assignee.user_id || assignee.id;
|
|
136
|
+
if (!id) return null;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
id,
|
|
140
|
+
display_name: assignee.display_name ?? undefined,
|
|
141
|
+
email: assignee.email ?? undefined,
|
|
142
|
+
avatar_url: assignee.avatar_url ?? undefined,
|
|
143
|
+
};
|
|
144
|
+
})
|
|
145
|
+
.filter((assignee): assignee is NonNullable<typeof assignee> =>
|
|
146
|
+
Boolean(assignee)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeTaskProjects(projects: WorkspaceTaskProject[]) {
|
|
151
|
+
return projects.map((project) => ({
|
|
152
|
+
id: project.id,
|
|
153
|
+
name: project.name,
|
|
154
|
+
status: project.status ?? 'unknown',
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
132
158
|
export function useTaskRelationships({
|
|
133
159
|
wsId,
|
|
134
160
|
labelCacheWorkspaceId,
|
|
@@ -189,9 +215,18 @@ export function useTaskRelationships({
|
|
|
189
215
|
});
|
|
190
216
|
|
|
191
217
|
setSelectedLabels(nextSelectedLabels);
|
|
192
|
-
|
|
218
|
+
patchTaskInVisibleCaches({
|
|
219
|
+
queryClient,
|
|
220
|
+
boardId,
|
|
221
|
+
taskId,
|
|
222
|
+
updater: (task) => ({
|
|
223
|
+
...task,
|
|
224
|
+
labels: nextSelectedLabels,
|
|
225
|
+
}),
|
|
226
|
+
});
|
|
193
227
|
queryClient.invalidateQueries({ queryKey: ['task-history'] });
|
|
194
228
|
broadcast?.('task:relations-changed', { taskId });
|
|
229
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
195
230
|
onUpdate();
|
|
196
231
|
} catch (error: unknown) {
|
|
197
232
|
toast.error('Label update failed', {
|
|
@@ -246,30 +281,18 @@ export function useTaskRelationships({
|
|
|
246
281
|
});
|
|
247
282
|
|
|
248
283
|
setSelectedAssignees(nextSelectedAssignees);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
: [
|
|
259
|
-
...currentAssignees,
|
|
260
|
-
{
|
|
261
|
-
id: userId,
|
|
262
|
-
display_name: member.display_name,
|
|
263
|
-
email: member.email,
|
|
264
|
-
avatar_url: member.avatar_url,
|
|
265
|
-
},
|
|
266
|
-
];
|
|
267
|
-
return { ...task, assignees: newAssignees };
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
);
|
|
284
|
+
patchTaskInVisibleCaches({
|
|
285
|
+
queryClient,
|
|
286
|
+
boardId,
|
|
287
|
+
taskId,
|
|
288
|
+
updater: (task) => ({
|
|
289
|
+
...task,
|
|
290
|
+
assignees: normalizeTaskAssignees(nextSelectedAssignees),
|
|
291
|
+
}),
|
|
292
|
+
});
|
|
271
293
|
queryClient.invalidateQueries({ queryKey: ['task-history'] });
|
|
272
294
|
broadcast?.('task:relations-changed', { taskId });
|
|
295
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
273
296
|
onUpdate();
|
|
274
297
|
} catch (error: unknown) {
|
|
275
298
|
toast.error('Assignee update failed', {
|
|
@@ -315,22 +338,18 @@ export function useTaskRelationships({
|
|
|
315
338
|
});
|
|
316
339
|
|
|
317
340
|
setSelectedProjects(nextSelectedProjects);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
: [...currentProjects, project];
|
|
328
|
-
return { ...task, projects: newProjects };
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
);
|
|
341
|
+
patchTaskInVisibleCaches({
|
|
342
|
+
queryClient,
|
|
343
|
+
boardId,
|
|
344
|
+
taskId,
|
|
345
|
+
updater: (task) => ({
|
|
346
|
+
...task,
|
|
347
|
+
projects: normalizeTaskProjects(nextSelectedProjects),
|
|
348
|
+
}),
|
|
349
|
+
});
|
|
332
350
|
queryClient.invalidateQueries({ queryKey: ['task-history'] });
|
|
333
351
|
broadcast?.('task:relations-changed', { taskId });
|
|
352
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
334
353
|
onUpdate();
|
|
335
354
|
} catch (error: unknown) {
|
|
336
355
|
toast.error('Project update failed', {
|
|
@@ -393,23 +412,17 @@ export function useTaskRelationships({
|
|
|
393
412
|
});
|
|
394
413
|
|
|
395
414
|
setSelectedLabels(nextSelectedLabels);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
labels: [...currentLabels, newLabel].sort((a, b) =>
|
|
406
|
-
compareLabelsByName(a, b)
|
|
407
|
-
),
|
|
408
|
-
};
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
);
|
|
415
|
+
patchTaskInVisibleCaches({
|
|
416
|
+
queryClient,
|
|
417
|
+
boardId,
|
|
418
|
+
taskId,
|
|
419
|
+
updater: (task) => ({
|
|
420
|
+
...task,
|
|
421
|
+
labels: nextSelectedLabels,
|
|
422
|
+
}),
|
|
423
|
+
});
|
|
412
424
|
broadcast?.('task:relations-changed', { taskId });
|
|
425
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
413
426
|
onUpdate();
|
|
414
427
|
toast.success('Label created & linked', {
|
|
415
428
|
description: 'New label added and attached to this task.',
|
|
@@ -485,21 +498,17 @@ export function useTaskRelationships({
|
|
|
485
498
|
});
|
|
486
499
|
|
|
487
500
|
setSelectedProjects(nextSelectedProjects);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
projects: [...currentProjects, newProject],
|
|
498
|
-
};
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
);
|
|
501
|
+
patchTaskInVisibleCaches({
|
|
502
|
+
queryClient,
|
|
503
|
+
boardId,
|
|
504
|
+
taskId,
|
|
505
|
+
updater: (task) => ({
|
|
506
|
+
...task,
|
|
507
|
+
projects: normalizeTaskProjects(nextSelectedProjects),
|
|
508
|
+
}),
|
|
509
|
+
});
|
|
502
510
|
broadcast?.('task:relations-changed', { taskId });
|
|
511
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
503
512
|
onUpdate();
|
|
504
513
|
toast.success('Project created & linked', {
|
|
505
514
|
description: 'New project added and attached to this task.',
|
|
@@ -38,6 +38,8 @@ interface PersonalOverridesSectionProps {
|
|
|
38
38
|
onUpdate?: () => void;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
type PersonalOverridePopoverId = 'priority' | 'estimation';
|
|
42
|
+
|
|
41
43
|
export function PersonalOverridesSection({
|
|
42
44
|
taskId,
|
|
43
45
|
isCreateMode,
|
|
@@ -51,10 +53,22 @@ export function PersonalOverridesSection({
|
|
|
51
53
|
onUpdate
|
|
52
54
|
);
|
|
53
55
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
54
|
-
const [
|
|
55
|
-
|
|
56
|
+
const [activePopover, setActivePopover] =
|
|
57
|
+
useState<PersonalOverridePopoverId | null>(null);
|
|
56
58
|
const [notes, setNotes] = useState('');
|
|
57
59
|
|
|
60
|
+
const isPopoverOpen = (popoverId: PersonalOverridePopoverId) =>
|
|
61
|
+
activePopover === popoverId;
|
|
62
|
+
const setPopoverOpen = (
|
|
63
|
+
popoverId: PersonalOverridePopoverId,
|
|
64
|
+
open: boolean
|
|
65
|
+
) => {
|
|
66
|
+
setActivePopover((currentPopover) => {
|
|
67
|
+
if (open) return popoverId;
|
|
68
|
+
return currentPopover === popoverId ? null : currentPopover;
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
58
72
|
// Don't render for new tasks
|
|
59
73
|
if (isCreateMode || !taskId) return null;
|
|
60
74
|
|
|
@@ -75,7 +89,7 @@ export function PersonalOverridesSection({
|
|
|
75
89
|
|
|
76
90
|
const handlePriorityChange = (priority: TaskPriority | null) => {
|
|
77
91
|
upsert({ priority_override: priority });
|
|
78
|
-
|
|
92
|
+
setPopoverOpen('priority', false);
|
|
79
93
|
};
|
|
80
94
|
|
|
81
95
|
const handleDueDateChange = (date: Date | undefined) => {
|
|
@@ -86,7 +100,7 @@ export function PersonalOverridesSection({
|
|
|
86
100
|
|
|
87
101
|
const handleEstimationChange = (points: number | null) => {
|
|
88
102
|
upsert({ estimation_override: points });
|
|
89
|
-
|
|
103
|
+
setPopoverOpen('estimation', false);
|
|
90
104
|
};
|
|
91
105
|
|
|
92
106
|
const handleNotesBlur = () => {
|
|
@@ -112,7 +126,10 @@ export function PersonalOverridesSection({
|
|
|
112
126
|
<div className="border-t bg-muted/20">
|
|
113
127
|
<button
|
|
114
128
|
type="button"
|
|
115
|
-
onClick={() =>
|
|
129
|
+
onClick={() => {
|
|
130
|
+
setIsExpanded(!isExpanded);
|
|
131
|
+
setActivePopover(null);
|
|
132
|
+
}}
|
|
116
133
|
className="flex w-full items-center justify-between px-4 py-2 text-left transition-colors hover:bg-muted/40 md:px-8"
|
|
117
134
|
>
|
|
118
135
|
<div className="flex items-center gap-2">
|
|
@@ -167,7 +184,10 @@ export function PersonalOverridesSection({
|
|
|
167
184
|
<Flag className="h-4 w-4 text-dynamic-orange" />
|
|
168
185
|
{t('ws-tasks.my_priority')}
|
|
169
186
|
</Label>
|
|
170
|
-
<Popover
|
|
187
|
+
<Popover
|
|
188
|
+
open={isPopoverOpen('priority')}
|
|
189
|
+
onOpenChange={(open) => setPopoverOpen('priority', open)}
|
|
190
|
+
>
|
|
171
191
|
<PopoverTrigger asChild>
|
|
172
192
|
<Button
|
|
173
193
|
variant="outline"
|
|
@@ -255,8 +275,8 @@ export function PersonalOverridesSection({
|
|
|
255
275
|
{t('ws-tasks.my_estimate')}
|
|
256
276
|
</Label>
|
|
257
277
|
<Popover
|
|
258
|
-
open={
|
|
259
|
-
onOpenChange={
|
|
278
|
+
open={isPopoverOpen('estimation')}
|
|
279
|
+
onOpenChange={(open) => setPopoverOpen('estimation', open)}
|
|
260
280
|
>
|
|
261
281
|
<PopoverTrigger asChild>
|
|
262
282
|
<Button
|
package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx
CHANGED
|
@@ -25,10 +25,15 @@ export function DependenciesSection({
|
|
|
25
25
|
onNavigateToTask,
|
|
26
26
|
onAddBlockingTaskDialog,
|
|
27
27
|
onAddBlockedByTaskDialog,
|
|
28
|
+
searchOpen: controlledSearchOpen,
|
|
29
|
+
onSearchOpenChange,
|
|
28
30
|
disabled,
|
|
29
31
|
}: DependenciesSectionProps) {
|
|
30
32
|
const [subTab, setSubTab] = React.useState<DependencySubTab>(initialSubTab);
|
|
31
|
-
const [
|
|
33
|
+
const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
|
|
34
|
+
React.useState(false);
|
|
35
|
+
const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
|
|
36
|
+
const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
|
|
32
37
|
|
|
33
38
|
const allExcludeIds = React.useMemo(() => {
|
|
34
39
|
const ids = new Set<string>();
|
|
@@ -56,7 +61,10 @@ export function DependenciesSection({
|
|
|
56
61
|
<Button
|
|
57
62
|
variant={subTab === 'blocks' ? 'default' : 'outline'}
|
|
58
63
|
size="sm"
|
|
59
|
-
onClick={() =>
|
|
64
|
+
onClick={() => {
|
|
65
|
+
setSubTab('blocks');
|
|
66
|
+
setSearchOpen(false);
|
|
67
|
+
}}
|
|
60
68
|
className="h-7 text-xs"
|
|
61
69
|
>
|
|
62
70
|
Blocks ({blockingTasks.length})
|
|
@@ -64,7 +72,10 @@ export function DependenciesSection({
|
|
|
64
72
|
<Button
|
|
65
73
|
variant={subTab === 'blocked-by' ? 'default' : 'outline'}
|
|
66
74
|
size="sm"
|
|
67
|
-
onClick={() =>
|
|
75
|
+
onClick={() => {
|
|
76
|
+
setSubTab('blocked-by');
|
|
77
|
+
setSearchOpen(false);
|
|
78
|
+
}}
|
|
68
79
|
className="h-7 text-xs"
|
|
69
80
|
>
|
|
70
81
|
Blocked By ({blockedByTasks.length})
|
|
@@ -16,9 +16,14 @@ export function ParentSection({
|
|
|
16
16
|
onRemoveParent,
|
|
17
17
|
onNavigateToTask,
|
|
18
18
|
onAddParentTask,
|
|
19
|
+
searchOpen: controlledSearchOpen,
|
|
20
|
+
onSearchOpenChange,
|
|
19
21
|
disabled,
|
|
20
22
|
}: ParentSectionProps) {
|
|
21
|
-
const [
|
|
23
|
+
const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
|
|
24
|
+
React.useState(false);
|
|
25
|
+
const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
|
|
26
|
+
const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
|
|
22
27
|
|
|
23
28
|
const excludeIds = React.useMemo(() => {
|
|
24
29
|
const ids = taskId ? [taskId, ...childTaskIds] : childTaskIds;
|
|
@@ -16,9 +16,14 @@ export function RelatedSection({
|
|
|
16
16
|
onRemoveRelated,
|
|
17
17
|
onNavigateToTask,
|
|
18
18
|
onAddRelatedTaskDialog,
|
|
19
|
+
searchOpen: controlledSearchOpen,
|
|
20
|
+
onSearchOpenChange,
|
|
19
21
|
disabled,
|
|
20
22
|
}: RelatedSectionProps) {
|
|
21
|
-
const [
|
|
23
|
+
const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
|
|
24
|
+
React.useState(false);
|
|
25
|
+
const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
|
|
26
|
+
const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
|
|
22
27
|
|
|
23
28
|
const excludeIds = React.useMemo(() => {
|
|
24
29
|
const ids = taskId ? [taskId] : [];
|
|
@@ -16,9 +16,14 @@ export function SubtasksSection({
|
|
|
16
16
|
onAddSubtask,
|
|
17
17
|
onAddExistingAsSubtask,
|
|
18
18
|
isSaving,
|
|
19
|
+
searchOpen: controlledSearchOpen,
|
|
20
|
+
onSearchOpenChange,
|
|
19
21
|
disabled,
|
|
20
22
|
}: SubtasksSectionProps) {
|
|
21
|
-
const [
|
|
23
|
+
const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
|
|
24
|
+
React.useState(false);
|
|
25
|
+
const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
|
|
26
|
+
const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
|
|
22
27
|
|
|
23
28
|
const excludeIds = React.useMemo(() => {
|
|
24
29
|
const ids = taskId ? [taskId] : [];
|
|
@@ -122,6 +122,8 @@ export interface ParentSectionProps {
|
|
|
122
122
|
onRemoveParent: () => void;
|
|
123
123
|
onNavigateToTask: (taskId: string) => void;
|
|
124
124
|
onAddParentTask?: () => void; // Opens dialog to create new parent task
|
|
125
|
+
searchOpen?: boolean;
|
|
126
|
+
onSearchOpenChange?: (open: boolean) => void;
|
|
125
127
|
disabled?: boolean;
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -137,6 +139,8 @@ export interface SubtasksSectionProps {
|
|
|
137
139
|
onAddSubtask?: () => void;
|
|
138
140
|
onAddExistingAsSubtask?: (task: RelatedTaskInfo) => Promise<void>;
|
|
139
141
|
isSaving: boolean;
|
|
142
|
+
searchOpen?: boolean;
|
|
143
|
+
onSearchOpenChange?: (open: boolean) => void;
|
|
140
144
|
disabled?: boolean;
|
|
141
145
|
}
|
|
142
146
|
|
|
@@ -168,6 +172,8 @@ export interface DependenciesSectionProps {
|
|
|
168
172
|
onNavigateToTask: (taskId: string) => void;
|
|
169
173
|
onAddBlockingTaskDialog?: () => void; // Opens dialog to create new blocking task
|
|
170
174
|
onAddBlockedByTaskDialog?: () => void; // Opens dialog to create new blocked-by task
|
|
175
|
+
searchOpen?: boolean;
|
|
176
|
+
onSearchOpenChange?: (open: boolean) => void;
|
|
171
177
|
disabled?: boolean;
|
|
172
178
|
}
|
|
173
179
|
|
|
@@ -182,5 +188,7 @@ export interface RelatedSectionProps {
|
|
|
182
188
|
onRemoveRelated: (taskId: string) => void;
|
|
183
189
|
onNavigateToTask: (taskId: string) => void;
|
|
184
190
|
onAddRelatedTaskDialog?: () => void; // Opens dialog to create new related task;
|
|
191
|
+
searchOpen?: boolean;
|
|
192
|
+
onSearchOpenChange?: (open: boolean) => void;
|
|
185
193
|
disabled?: boolean;
|
|
186
194
|
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
UserMinus,
|
|
23
23
|
UserPlus,
|
|
24
24
|
} from '@tuturuuu/icons';
|
|
25
|
+
import { getWorkspaceTaskHistory } from '@tuturuuu/internal-api/tasks';
|
|
25
26
|
import { Avatar, AvatarFallback, AvatarImage } from '@tuturuuu/ui/avatar';
|
|
26
27
|
import { Badge } from '@tuturuuu/ui/badge';
|
|
27
28
|
import { Button } from '@tuturuuu/ui/button';
|
|
@@ -34,6 +35,9 @@ import { useLocale, useTranslations } from 'next-intl';
|
|
|
34
35
|
import { useState } from 'react';
|
|
35
36
|
import type { EstimationType } from '../estimation-mapping';
|
|
36
37
|
import { mapEstimationPoints } from '../estimation-mapping';
|
|
38
|
+
import type { RecoverableTaskDescriptionVersion } from './description-versions';
|
|
39
|
+
import { isTaskDescriptionHistoryEntry } from './description-versions';
|
|
40
|
+
import { TaskDescriptionChangeDialog } from './task-description-change-dialog';
|
|
37
41
|
import { TaskSnapshotDialog } from './task-snapshot-dialog';
|
|
38
42
|
|
|
39
43
|
/** Represents a task history entry from the API */
|
|
@@ -77,6 +81,11 @@ interface TaskActivitySectionProps {
|
|
|
77
81
|
estimationType?: EstimationType;
|
|
78
82
|
/** When true, disables the revert functionality (feature not stable) */
|
|
79
83
|
revertDisabled?: boolean;
|
|
84
|
+
/** Restores a tracked description version through the Yjs-safe description path. */
|
|
85
|
+
onRestoreDescriptionVersion?: (
|
|
86
|
+
version: RecoverableTaskDescriptionVersion
|
|
87
|
+
) => Promise<void> | void;
|
|
88
|
+
restoringDescriptionVersionId?: string | null;
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
// Task history section for showing activity logs and snapshots
|
|
@@ -91,6 +100,8 @@ export function TaskActivitySection({
|
|
|
91
100
|
onTaskUpdate,
|
|
92
101
|
estimationType,
|
|
93
102
|
revertDisabled = false,
|
|
103
|
+
onRestoreDescriptionVersion,
|
|
104
|
+
restoringDescriptionVersionId,
|
|
94
105
|
}: TaskActivitySectionProps) {
|
|
95
106
|
const t = useTranslations('tasks-history');
|
|
96
107
|
const snapshotT = useTranslations('tasks.history');
|
|
@@ -100,6 +111,8 @@ export function TaskActivitySection({
|
|
|
100
111
|
const [snapshotEntry, setSnapshotEntry] = useState<TaskHistoryEntry | null>(
|
|
101
112
|
null
|
|
102
113
|
);
|
|
114
|
+
const [descriptionChangeEntry, setDescriptionChangeEntry] =
|
|
115
|
+
useState<TaskHistoryEntry | null>(null);
|
|
103
116
|
const dateLocale = locale === 'vi' ? vi : enUS;
|
|
104
117
|
|
|
105
118
|
// Fetch task history
|
|
@@ -107,12 +120,9 @@ export function TaskActivitySection({
|
|
|
107
120
|
queryKey: ['task-history', wsId, taskId],
|
|
108
121
|
queryFn: async () => {
|
|
109
122
|
if (!taskId) return null;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
114
|
-
if (!res.ok) throw new Error('Failed to fetch task history');
|
|
115
|
-
return res.json() as Promise<{
|
|
123
|
+
return getWorkspaceTaskHistory(wsId, taskId, {
|
|
124
|
+
limit: 50,
|
|
125
|
+
}) as Promise<{
|
|
116
126
|
history: TaskHistoryEntry[];
|
|
117
127
|
count: number;
|
|
118
128
|
}>;
|
|
@@ -179,7 +189,21 @@ export function TaskActivitySection({
|
|
|
179
189
|
}
|
|
180
190
|
dateLocale={dateLocale}
|
|
181
191
|
showActions={!!currentTask && !!boardId}
|
|
182
|
-
onViewSnapshot={() =>
|
|
192
|
+
onViewSnapshot={() => {
|
|
193
|
+
if (isTaskDescriptionHistoryEntry(entry)) {
|
|
194
|
+
setDescriptionChangeEntry(entry);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
setSnapshotEntry(entry);
|
|
199
|
+
}}
|
|
200
|
+
viewLabel={
|
|
201
|
+
isTaskDescriptionHistoryEntry(entry)
|
|
202
|
+
? t('view_description_change', {
|
|
203
|
+
defaultValue: 'View description change',
|
|
204
|
+
})
|
|
205
|
+
: t('view_snapshot')
|
|
206
|
+
}
|
|
183
207
|
estimationType={estimationType}
|
|
184
208
|
/>
|
|
185
209
|
))}
|
|
@@ -230,6 +254,28 @@ export function TaskActivitySection({
|
|
|
230
254
|
revertDisabled={revertDisabled}
|
|
231
255
|
/>
|
|
232
256
|
)}
|
|
257
|
+
|
|
258
|
+
{descriptionChangeEntry && (
|
|
259
|
+
<TaskDescriptionChangeDialog
|
|
260
|
+
canRestore={Boolean(onRestoreDescriptionVersion)}
|
|
261
|
+
entry={descriptionChangeEntry}
|
|
262
|
+
isOpen={!!descriptionChangeEntry}
|
|
263
|
+
locale={locale}
|
|
264
|
+
onClose={() => setDescriptionChangeEntry(null)}
|
|
265
|
+
onRestoreVersion={async (version) => {
|
|
266
|
+
await onRestoreDescriptionVersion?.(version);
|
|
267
|
+
setDescriptionChangeEntry(null);
|
|
268
|
+
onTaskUpdate?.();
|
|
269
|
+
}}
|
|
270
|
+
restoringVersionId={restoringDescriptionVersionId}
|
|
271
|
+
t={
|
|
272
|
+
snapshotT as (
|
|
273
|
+
key: string,
|
|
274
|
+
options?: { count?: number; defaultValue?: string }
|
|
275
|
+
) => string
|
|
276
|
+
}
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
233
279
|
</div>
|
|
234
280
|
);
|
|
235
281
|
}
|
|
@@ -240,6 +286,7 @@ interface ActivityEntryProps {
|
|
|
240
286
|
dateLocale: typeof enUS | typeof vi;
|
|
241
287
|
showActions?: boolean;
|
|
242
288
|
onViewSnapshot?: () => void;
|
|
289
|
+
viewLabel?: string;
|
|
243
290
|
estimationType?: EstimationType;
|
|
244
291
|
}
|
|
245
292
|
|
|
@@ -249,6 +296,7 @@ function ActivityEntry({
|
|
|
249
296
|
dateLocale,
|
|
250
297
|
showActions = false,
|
|
251
298
|
onViewSnapshot,
|
|
299
|
+
viewLabel,
|
|
252
300
|
estimationType,
|
|
253
301
|
}: ActivityEntryProps) {
|
|
254
302
|
const { icon, color } = getChangeIcon(entry.change_type, entry.field_name);
|
|
@@ -338,7 +386,7 @@ function ActivityEntry({
|
|
|
338
386
|
</Button>
|
|
339
387
|
</TooltipTrigger>
|
|
340
388
|
<TooltipContent side="top" className="text-xs">
|
|
341
|
-
{t('view_snapshot')}
|
|
389
|
+
{viewLabel ?? t('view_snapshot')}
|
|
342
390
|
</TooltipContent>
|
|
343
391
|
</Tooltip>
|
|
344
392
|
</div>
|
package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.test.tsx
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest';
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { TaskDescriptionChangeDialog } from './task-description-change-dialog';
|
|
6
|
+
|
|
7
|
+
vi.mock('./description-diff-viewer', () => ({
|
|
8
|
+
DescriptionDiffViewer: () => <button type="button">view-diff</button>,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const t = (key: string, options?: { defaultValue?: string }) =>
|
|
12
|
+
options?.defaultValue ?? key;
|
|
13
|
+
|
|
14
|
+
const makeDoc = (text: string) =>
|
|
15
|
+
JSON.stringify({
|
|
16
|
+
type: 'doc',
|
|
17
|
+
content: [
|
|
18
|
+
{
|
|
19
|
+
type: 'paragraph',
|
|
20
|
+
content: [{ type: 'text', text }],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('TaskDescriptionChangeDialog', () => {
|
|
26
|
+
it('offers restoring the previous recoverable description from a wipe row', async () => {
|
|
27
|
+
const onRestoreVersion = vi.fn();
|
|
28
|
+
|
|
29
|
+
render(
|
|
30
|
+
<TaskDescriptionChangeDialog
|
|
31
|
+
entry={{
|
|
32
|
+
id: 'history-1',
|
|
33
|
+
task_id: 'task-1',
|
|
34
|
+
changed_by: 'user-1',
|
|
35
|
+
changed_at: '2026-06-27T00:00:00.000Z',
|
|
36
|
+
change_type: 'field_updated',
|
|
37
|
+
field_name: 'description',
|
|
38
|
+
old_value: makeDoc('Recover me'),
|
|
39
|
+
new_value: null,
|
|
40
|
+
metadata: {},
|
|
41
|
+
user: { id: 'user-1', name: 'User' },
|
|
42
|
+
}}
|
|
43
|
+
isOpen
|
|
44
|
+
onClose={vi.fn()}
|
|
45
|
+
onRestoreVersion={onRestoreVersion}
|
|
46
|
+
t={t}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(screen.getByText('Recover me')).toBeInTheDocument();
|
|
51
|
+
|
|
52
|
+
fireEvent.click(screen.getByRole('button', { name: /restore previous/i }));
|
|
53
|
+
|
|
54
|
+
await waitFor(() => {
|
|
55
|
+
expect(onRestoreVersion).toHaveBeenCalledWith(
|
|
56
|
+
expect.objectContaining({
|
|
57
|
+
previewText: 'Recover me',
|
|
58
|
+
source: 'old_value',
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|