@tuturuuu/ui 0.9.0 → 0.10.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 +29 -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/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 +116 -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 +21 -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 +14 -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 +4 -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-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/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-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.tsx +9 -0
- 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/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
|
@@ -6,13 +6,17 @@ import {
|
|
|
6
6
|
import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
|
|
7
7
|
import type { CalendarHoursType } from '@tuturuuu/types/primitives/Task';
|
|
8
8
|
import { useToast } from '@tuturuuu/ui/hooks/use-toast';
|
|
9
|
-
import { invalidateTaskCaches } from '@tuturuuu/utils/task-helper';
|
|
10
9
|
import { useCallback, useState } from 'react';
|
|
11
10
|
import {
|
|
12
11
|
type BoardBroadcastFn,
|
|
13
12
|
getActiveBroadcast,
|
|
14
13
|
useBoardBroadcast,
|
|
15
14
|
} from '../../board-broadcast-context';
|
|
15
|
+
import {
|
|
16
|
+
patchTaskInVisibleCaches,
|
|
17
|
+
restoreVisibleTaskCaches,
|
|
18
|
+
snapshotVisibleTaskCaches,
|
|
19
|
+
} from '../../task-cache-patches';
|
|
16
20
|
import { updateWorkspaceTask } from './task-api';
|
|
17
21
|
|
|
18
22
|
export interface SchedulingSettings {
|
|
@@ -109,17 +113,17 @@ export function useTaskMutations({
|
|
|
109
113
|
return;
|
|
110
114
|
}
|
|
111
115
|
setEstimationSaving(true);
|
|
116
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
117
|
+
taskId,
|
|
118
|
+
]);
|
|
112
119
|
|
|
113
120
|
// Optimistic update - prevents flicker by updating cache immediately
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
);
|
|
121
|
+
patchTaskInVisibleCaches({
|
|
122
|
+
queryClient,
|
|
123
|
+
boardId,
|
|
124
|
+
taskId,
|
|
125
|
+
updater: (task) => ({ ...task, estimation_points: points }),
|
|
126
|
+
});
|
|
123
127
|
|
|
124
128
|
try {
|
|
125
129
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
@@ -135,7 +139,7 @@ export function useTaskMutations({
|
|
|
135
139
|
} catch (e: any) {
|
|
136
140
|
console.error('Failed updating estimation', e);
|
|
137
141
|
// Revert optimistic update on error
|
|
138
|
-
|
|
142
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
139
143
|
toast({
|
|
140
144
|
title: 'Failed to update estimation',
|
|
141
145
|
description: e.message || 'Please try again',
|
|
@@ -166,17 +170,17 @@ export function useTaskMutations({
|
|
|
166
170
|
if (isCreateMode || !taskId || taskId === 'new') {
|
|
167
171
|
return;
|
|
168
172
|
}
|
|
173
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
174
|
+
taskId,
|
|
175
|
+
]);
|
|
169
176
|
|
|
170
177
|
// Optimistic update - prevents flicker
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
);
|
|
178
|
+
patchTaskInVisibleCaches({
|
|
179
|
+
queryClient,
|
|
180
|
+
boardId,
|
|
181
|
+
taskId,
|
|
182
|
+
updater: (task) => ({ ...task, priority: newPriority }),
|
|
183
|
+
});
|
|
180
184
|
|
|
181
185
|
try {
|
|
182
186
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
@@ -192,7 +196,7 @@ export function useTaskMutations({
|
|
|
192
196
|
} catch (e: any) {
|
|
193
197
|
console.error('Failed updating priority', e);
|
|
194
198
|
// Revert optimistic update on error
|
|
195
|
-
|
|
199
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
196
200
|
toast({
|
|
197
201
|
title: 'Failed to update priority',
|
|
198
202
|
description: e.message || 'Please try again',
|
|
@@ -222,17 +226,17 @@ export function useTaskMutations({
|
|
|
222
226
|
}
|
|
223
227
|
|
|
224
228
|
const dateString = newDate ? newDate.toISOString() : null;
|
|
229
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
230
|
+
taskId,
|
|
231
|
+
]);
|
|
225
232
|
|
|
226
233
|
// Optimistic update - prevents flicker
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
);
|
|
234
|
+
patchTaskInVisibleCaches({
|
|
235
|
+
queryClient,
|
|
236
|
+
boardId,
|
|
237
|
+
taskId,
|
|
238
|
+
updater: (task) => ({ ...task, start_date: dateString ?? undefined }),
|
|
239
|
+
});
|
|
236
240
|
|
|
237
241
|
try {
|
|
238
242
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
@@ -248,7 +252,7 @@ export function useTaskMutations({
|
|
|
248
252
|
} catch (e: any) {
|
|
249
253
|
console.error('Failed updating start date', e);
|
|
250
254
|
// Revert optimistic update on error
|
|
251
|
-
|
|
255
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
252
256
|
toast({
|
|
253
257
|
title: 'Failed to update start date',
|
|
254
258
|
description: e.message || 'Please try again',
|
|
@@ -277,17 +281,17 @@ export function useTaskMutations({
|
|
|
277
281
|
}
|
|
278
282
|
|
|
279
283
|
const dateString = newDate ? newDate.toISOString() : null;
|
|
284
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
285
|
+
taskId,
|
|
286
|
+
]);
|
|
280
287
|
|
|
281
288
|
// Optimistic update - prevents flicker
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
);
|
|
289
|
+
patchTaskInVisibleCaches({
|
|
290
|
+
queryClient,
|
|
291
|
+
boardId,
|
|
292
|
+
taskId,
|
|
293
|
+
updater: (task) => ({ ...task, end_date: dateString }),
|
|
294
|
+
});
|
|
291
295
|
|
|
292
296
|
try {
|
|
293
297
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
@@ -303,7 +307,7 @@ export function useTaskMutations({
|
|
|
303
307
|
} catch (e: any) {
|
|
304
308
|
console.error('Failed updating end date', e);
|
|
305
309
|
// Revert optimistic update on error
|
|
306
|
-
|
|
310
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
307
311
|
toast({
|
|
308
312
|
title: 'Failed to update end date',
|
|
309
313
|
description: e.message || 'Please try again',
|
|
@@ -331,38 +335,33 @@ export function useTaskMutations({
|
|
|
331
335
|
if (isCreateMode || !taskId || taskId === 'new') {
|
|
332
336
|
return;
|
|
333
337
|
}
|
|
338
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
339
|
+
taskId,
|
|
340
|
+
]);
|
|
334
341
|
|
|
335
342
|
// Optimistic update - prevents flicker
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
);
|
|
343
|
+
patchTaskInVisibleCaches({
|
|
344
|
+
queryClient,
|
|
345
|
+
boardId,
|
|
346
|
+
taskId,
|
|
347
|
+
updater: (task) => ({ ...task, list_id: newListId }),
|
|
348
|
+
});
|
|
345
349
|
|
|
346
350
|
try {
|
|
347
351
|
const { task: updatedTask } = await updateWorkspaceTask(wsId, taskId, {
|
|
348
352
|
list_id: newListId,
|
|
349
353
|
});
|
|
350
354
|
// Update sender's own cache with DB-computed timestamps
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
: task
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
);
|
|
355
|
+
patchTaskInVisibleCaches({
|
|
356
|
+
queryClient,
|
|
357
|
+
boardId,
|
|
358
|
+
taskId,
|
|
359
|
+
updater: (task) => ({
|
|
360
|
+
...task,
|
|
361
|
+
completed_at: updatedTask.completed_at,
|
|
362
|
+
closed_at: updatedTask.closed_at,
|
|
363
|
+
}),
|
|
364
|
+
});
|
|
366
365
|
broadcast?.('task:upsert', {
|
|
367
366
|
task: {
|
|
368
367
|
id: taskId,
|
|
@@ -379,7 +378,7 @@ export function useTaskMutations({
|
|
|
379
378
|
} catch (e: any) {
|
|
380
379
|
console.error('Failed updating list', e);
|
|
381
380
|
// Revert optimistic update on error
|
|
382
|
-
|
|
381
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
383
382
|
toast({
|
|
384
383
|
title: 'Failed to update list',
|
|
385
384
|
description: e.message || 'Please try again',
|
|
@@ -412,15 +411,15 @@ export function useTaskMutations({
|
|
|
412
411
|
|
|
413
412
|
// Optimistically update the cache instead of invalidating
|
|
414
413
|
// This prevents conflicts with realtime sync
|
|
415
|
-
queryClient
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
);
|
|
414
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
415
|
+
taskId,
|
|
416
|
+
]);
|
|
417
|
+
patchTaskInVisibleCaches({
|
|
418
|
+
queryClient,
|
|
419
|
+
boardId,
|
|
420
|
+
taskId,
|
|
421
|
+
updater: (task) => ({ ...task, name: trimmedName }),
|
|
422
|
+
});
|
|
424
423
|
|
|
425
424
|
try {
|
|
426
425
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
@@ -432,8 +431,8 @@ export function useTaskMutations({
|
|
|
432
431
|
triggerRefresh();
|
|
433
432
|
} catch (e: any) {
|
|
434
433
|
console.error('Failed updating task name', e);
|
|
435
|
-
// Revert optimistic update
|
|
436
|
-
|
|
434
|
+
// Revert optimistic update without refetching visible board caches.
|
|
435
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
437
436
|
toast({
|
|
438
437
|
title: 'Failed to update task name',
|
|
439
438
|
description: e.message || 'Please try again',
|
package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx
CHANGED
|
@@ -146,4 +146,67 @@ describe('useTaskRelationships', () => {
|
|
|
146
146
|
])
|
|
147
147
|
).toEqual([newLabel, existingLabel]);
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
it('patches visible task caches when labels change without invalidating board task queries', async () => {
|
|
151
|
+
const cachedTask = {
|
|
152
|
+
id: 'task-1',
|
|
153
|
+
labels: [],
|
|
154
|
+
} as unknown as Task;
|
|
155
|
+
|
|
156
|
+
queryClient.setQueryData(['tasks', 'board-1'], [cachedTask]);
|
|
157
|
+
queryClient.setQueryData(['tasks-full', 'board-1', 'all'], [cachedTask]);
|
|
158
|
+
queryClient.setQueryData(['task', 'task-1'], cachedTask);
|
|
159
|
+
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
160
|
+
|
|
161
|
+
const { result } = renderHook(
|
|
162
|
+
() =>
|
|
163
|
+
useTaskRelationships({
|
|
164
|
+
wsId: 'personal',
|
|
165
|
+
taskId: 'task-1',
|
|
166
|
+
isCreateMode: false,
|
|
167
|
+
boardId: 'board-1',
|
|
168
|
+
selectedLabels: [],
|
|
169
|
+
selectedAssignees: [],
|
|
170
|
+
selectedProjects: [],
|
|
171
|
+
newLabelName: '',
|
|
172
|
+
newLabelColor: '#14b8a6',
|
|
173
|
+
newProjectName: '',
|
|
174
|
+
setSelectedLabels: vi.fn(),
|
|
175
|
+
setSelectedAssignees: vi.fn(),
|
|
176
|
+
setSelectedProjects: vi.fn(),
|
|
177
|
+
setAvailableLabels: vi.fn(),
|
|
178
|
+
setNewLabelName: vi.fn(),
|
|
179
|
+
setNewLabelColor: vi.fn(),
|
|
180
|
+
setNewProjectName: vi.fn(),
|
|
181
|
+
setShowNewLabelDialog: vi.fn(),
|
|
182
|
+
setShowNewProjectDialog: vi.fn(),
|
|
183
|
+
onUpdate: vi.fn(),
|
|
184
|
+
}),
|
|
185
|
+
{ wrapper }
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
await act(async () => {
|
|
189
|
+
await result.current.toggleLabel(newLabel);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(
|
|
193
|
+
queryClient.getQueryData<Task[]>(['tasks', 'board-1'])?.[0]?.labels
|
|
194
|
+
).toEqual([newLabel]);
|
|
195
|
+
expect(
|
|
196
|
+
queryClient.getQueryData<Task[]>(['tasks-full', 'board-1', 'all'])?.[0]
|
|
197
|
+
?.labels
|
|
198
|
+
).toEqual([newLabel]);
|
|
199
|
+
expect(queryClient.getQueryData<Task>(['task', 'task-1'])?.labels).toEqual([
|
|
200
|
+
newLabel,
|
|
201
|
+
]);
|
|
202
|
+
expect(invalidateSpy).not.toHaveBeenCalledWith({
|
|
203
|
+
queryKey: ['tasks', 'board-1'],
|
|
204
|
+
});
|
|
205
|
+
expect(invalidateSpy).not.toHaveBeenCalledWith({
|
|
206
|
+
queryKey: ['tasks-full', 'board-1'],
|
|
207
|
+
});
|
|
208
|
+
expect(mocks.broadcast).toHaveBeenCalledWith('task:relations-changed', {
|
|
209
|
+
taskId: 'task-1',
|
|
210
|
+
});
|
|
211
|
+
});
|
|
149
212
|
});
|
|
@@ -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})
|