@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
|
@@ -9,7 +9,17 @@ import type { TaskLabel as DbTaskLabel } from '@tuturuuu/types/db';
|
|
|
9
9
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
10
10
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
11
11
|
import { useState } from 'react';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getActiveBoardRefresh,
|
|
14
|
+
useBoardBroadcast,
|
|
15
|
+
} from '../shared/board-broadcast-context';
|
|
16
|
+
import {
|
|
17
|
+
getTaskFromVisibleCaches,
|
|
18
|
+
patchTaskInVisibleCaches,
|
|
19
|
+
restoreTasksFromVisibleCacheSnapshot,
|
|
20
|
+
restoreVisibleTaskCaches,
|
|
21
|
+
snapshotVisibleTaskCaches,
|
|
22
|
+
} from '../shared/task-cache-patches';
|
|
13
23
|
import { getRandomNewLabelColor } from '../utils/taskConstants';
|
|
14
24
|
|
|
15
25
|
type WorkspaceTaskLabel = Pick<
|
|
@@ -68,10 +78,14 @@ export function useTaskLabelManagement({
|
|
|
68
78
|
|
|
69
79
|
// CRITICAL: Get current task state from cache instead of stale prop
|
|
70
80
|
// This ensures we read the most up-to-date state after optimistic updates
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
const canonicalTaskId = taskId ?? task.id;
|
|
82
|
+
const currentTask =
|
|
83
|
+
getTaskFromVisibleCaches({
|
|
84
|
+
queryClient,
|
|
85
|
+
boardId,
|
|
86
|
+
taskId: canonicalTaskId,
|
|
87
|
+
fallback: task,
|
|
88
|
+
}) ?? task;
|
|
75
89
|
|
|
76
90
|
// Check if we're in multi-select mode with multiple tasks selected
|
|
77
91
|
const shouldBulkUpdate =
|
|
@@ -86,11 +100,17 @@ export function useTaskLabelManagement({
|
|
|
86
100
|
|
|
87
101
|
// Cancel any outgoing refetches
|
|
88
102
|
await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
|
|
103
|
+
await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
|
|
89
104
|
|
|
90
105
|
// Snapshot the previous value BEFORE optimistic update
|
|
91
106
|
const previousTasks = queryClient.getQueryData(['tasks', boardId]) as
|
|
92
107
|
| Task[]
|
|
93
108
|
| undefined;
|
|
109
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
110
|
+
queryClient,
|
|
111
|
+
boardId,
|
|
112
|
+
tasksToUpdate
|
|
113
|
+
);
|
|
94
114
|
|
|
95
115
|
// Determine action: remove if ALL selected tasks have the label, add otherwise
|
|
96
116
|
// Use currentTask from cache, not stale task prop
|
|
@@ -112,6 +132,13 @@ export function useTaskLabelManagement({
|
|
|
112
132
|
const fromBoardCache = previousTasks?.find((ct) => ct.id === taskId);
|
|
113
133
|
if (fromBoardCache) return fromBoardCache;
|
|
114
134
|
|
|
135
|
+
const fromVisibleCaches = getTaskFromVisibleCaches({
|
|
136
|
+
queryClient,
|
|
137
|
+
boardId,
|
|
138
|
+
taskId,
|
|
139
|
+
});
|
|
140
|
+
if (fromVisibleCaches) return fromVisibleCaches;
|
|
141
|
+
|
|
115
142
|
// Fallback to individual task cache (for tasks not in board view)
|
|
116
143
|
if (taskId === currentTask.id) return currentTask;
|
|
117
144
|
|
|
@@ -135,63 +162,36 @@ export function useTaskLabelManagement({
|
|
|
135
162
|
|
|
136
163
|
// Get label details from workspace labels for optimistic update
|
|
137
164
|
const label = workspaceLabels.find((l) => l.id === labelId);
|
|
165
|
+
const fallbackLabel = label || {
|
|
166
|
+
id: labelId,
|
|
167
|
+
name: 'Unknown',
|
|
168
|
+
color: '#3b82f6',
|
|
169
|
+
created_at: new Date().toISOString(),
|
|
170
|
+
};
|
|
138
171
|
|
|
139
172
|
// Optimistically update the cache - only update tasks that actually change
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
name: 'Unknown',
|
|
158
|
-
color: '#3b82f6',
|
|
159
|
-
created_at: new Date().toISOString(),
|
|
160
|
-
},
|
|
161
|
-
],
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return t;
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// CRITICAL: Also update the individual task cache if taskId is provided
|
|
169
|
-
// This ensures the chip menu's task cache stays in sync with the board cache
|
|
170
|
-
if (taskId) {
|
|
171
|
-
queryClient.setQueryData(['task', taskId], (old: Task | undefined) => {
|
|
172
|
-
if (!old) return old;
|
|
173
|
-
if (active && tasksToRemoveFrom.includes(taskId)) {
|
|
174
|
-
// Remove the label
|
|
175
|
-
return {
|
|
176
|
-
...old,
|
|
177
|
-
labels: old.labels?.filter((l) => l.id !== labelId) || [],
|
|
178
|
-
};
|
|
179
|
-
} else if (!active && tasksNeedingLabel.includes(taskId)) {
|
|
180
|
-
// Add the label
|
|
173
|
+
for (const tid of active ? tasksToRemoveFrom : tasksNeedingLabel) {
|
|
174
|
+
patchTaskInVisibleCaches({
|
|
175
|
+
queryClient,
|
|
176
|
+
boardId,
|
|
177
|
+
taskId: tid,
|
|
178
|
+
updater: (cachedTask) => {
|
|
179
|
+
if (active) {
|
|
180
|
+
return {
|
|
181
|
+
...cachedTask,
|
|
182
|
+
labels: cachedTask.labels?.filter((l) => l.id !== labelId) || [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (cachedTask.labels?.some((l) => l.id === labelId)) {
|
|
187
|
+
return cachedTask;
|
|
188
|
+
}
|
|
189
|
+
|
|
181
190
|
return {
|
|
182
|
-
...
|
|
183
|
-
labels: [
|
|
184
|
-
...(old.labels || []),
|
|
185
|
-
label || {
|
|
186
|
-
id: labelId,
|
|
187
|
-
name: 'Unknown',
|
|
188
|
-
color: '#3b82f6',
|
|
189
|
-
created_at: new Date().toISOString(),
|
|
190
|
-
},
|
|
191
|
-
],
|
|
191
|
+
...cachedTask,
|
|
192
|
+
labels: [...(cachedTask.labels || []), fallbackLabel],
|
|
192
193
|
};
|
|
193
|
-
}
|
|
194
|
-
return old;
|
|
194
|
+
},
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
|
|
@@ -201,33 +201,36 @@ export function useTaskLabelManagement({
|
|
|
201
201
|
? { baseUrl: window.location.origin }
|
|
202
202
|
: undefined;
|
|
203
203
|
let successCount = 0;
|
|
204
|
+
const succeededTaskIds: string[] = [];
|
|
204
205
|
|
|
205
206
|
if (active) {
|
|
206
|
-
for (const
|
|
207
|
+
for (const tid of tasksToRemoveFrom) {
|
|
207
208
|
try {
|
|
208
209
|
await removeWorkspaceTaskLabel(
|
|
209
210
|
workspaceId,
|
|
210
|
-
|
|
211
|
+
tid,
|
|
211
212
|
labelId,
|
|
212
213
|
internalApiOptions
|
|
213
214
|
);
|
|
214
215
|
successCount++;
|
|
216
|
+
succeededTaskIds.push(tid);
|
|
215
217
|
} catch (error) {
|
|
216
|
-
console.error(`Failed to remove label from task ${
|
|
218
|
+
console.error(`Failed to remove label from task ${tid}:`, error);
|
|
217
219
|
}
|
|
218
220
|
}
|
|
219
221
|
} else {
|
|
220
|
-
for (const
|
|
222
|
+
for (const tid of tasksNeedingLabel) {
|
|
221
223
|
try {
|
|
222
224
|
await addWorkspaceTaskLabel(
|
|
223
225
|
workspaceId,
|
|
224
|
-
|
|
226
|
+
tid,
|
|
225
227
|
labelId,
|
|
226
228
|
internalApiOptions
|
|
227
229
|
);
|
|
228
230
|
successCount++;
|
|
231
|
+
succeededTaskIds.push(tid);
|
|
229
232
|
} catch (error) {
|
|
230
|
-
console.error(`Failed to add label to task ${
|
|
233
|
+
console.error(`Failed to add label to task ${tid}:`, error);
|
|
231
234
|
}
|
|
232
235
|
}
|
|
233
236
|
}
|
|
@@ -240,10 +243,22 @@ export function useTaskLabelManagement({
|
|
|
240
243
|
throw new Error('Failed to update any tasks');
|
|
241
244
|
}
|
|
242
245
|
|
|
246
|
+
const failedTaskIds = (
|
|
247
|
+
active ? tasksToRemoveFrom : tasksNeedingLabel
|
|
248
|
+
).filter((tid) => !succeededTaskIds.includes(tid));
|
|
249
|
+
restoreTasksFromVisibleCacheSnapshot({
|
|
250
|
+
queryClient,
|
|
251
|
+
snapshot: cacheSnapshot,
|
|
252
|
+
taskIds: failedTaskIds,
|
|
253
|
+
});
|
|
254
|
+
|
|
243
255
|
// Broadcast relation changes for all affected tasks
|
|
244
|
-
for (const tid of
|
|
256
|
+
for (const tid of succeededTaskIds) {
|
|
245
257
|
broadcast?.('task:relations-changed', { taskId: tid });
|
|
246
258
|
}
|
|
259
|
+
if (succeededTaskIds.length > 0) {
|
|
260
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
261
|
+
}
|
|
247
262
|
|
|
248
263
|
toast.success(active ? 'Label removed' : 'Label added', {
|
|
249
264
|
description:
|
|
@@ -253,9 +268,7 @@ export function useTaskLabelManagement({
|
|
|
253
268
|
// Don't auto-clear selection - let user manually clear with "Clear" button
|
|
254
269
|
} catch (e: any) {
|
|
255
270
|
// Rollback on error
|
|
256
|
-
|
|
257
|
-
queryClient.setQueryData(['tasks', boardId], previousTasks);
|
|
258
|
-
}
|
|
271
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
259
272
|
console.error('Failed to toggle label:', e);
|
|
260
273
|
toast.error('Error', {
|
|
261
274
|
description: 'Failed to update label. Please try again.',
|
|
@@ -306,48 +319,44 @@ export function useTaskLabelManagement({
|
|
|
306
319
|
|
|
307
320
|
// Auto-apply the newly created label to this task
|
|
308
321
|
let linkSucceeded = false;
|
|
309
|
-
|
|
322
|
+
const canonicalTaskId = taskId ?? task.id;
|
|
323
|
+
let cacheSnapshot:
|
|
324
|
+
| ReturnType<typeof snapshotVisibleTaskCaches>
|
|
325
|
+
| undefined;
|
|
310
326
|
try {
|
|
311
327
|
// Cancel any outgoing refetches
|
|
312
328
|
await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
|
|
329
|
+
await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
|
|
313
330
|
|
|
314
331
|
// Snapshot the previous value
|
|
315
|
-
|
|
332
|
+
cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
333
|
+
canonicalTaskId,
|
|
334
|
+
]);
|
|
316
335
|
|
|
317
336
|
// Optimistically update the cache
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
...t,
|
|
326
|
-
labels: [...(t.labels || []), newLabel],
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
return t;
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
// CRITICAL: Also update individual task cache if taskId is provided
|
|
335
|
-
if (taskId) {
|
|
336
|
-
queryClient.setQueryData(
|
|
337
|
-
['task', taskId],
|
|
338
|
-
(old: Task | undefined) => {
|
|
339
|
-
if (!old) return old;
|
|
340
|
-
return {
|
|
341
|
-
...old,
|
|
342
|
-
labels: [...(old.labels || []), newLabel],
|
|
343
|
-
};
|
|
337
|
+
patchTaskInVisibleCaches({
|
|
338
|
+
queryClient,
|
|
339
|
+
boardId,
|
|
340
|
+
taskId: canonicalTaskId,
|
|
341
|
+
updater: (cachedTask) => {
|
|
342
|
+
if (cachedTask.labels?.some((label) => label.id === newLabel.id)) {
|
|
343
|
+
return cachedTask;
|
|
344
344
|
}
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
...cachedTask,
|
|
348
|
+
labels: [...(cachedTask.labels || []), newLabel],
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
});
|
|
347
352
|
|
|
348
353
|
const taskState =
|
|
349
|
-
(
|
|
350
|
-
|
|
354
|
+
getTaskFromVisibleCaches({
|
|
355
|
+
queryClient,
|
|
356
|
+
boardId,
|
|
357
|
+
taskId: canonicalTaskId,
|
|
358
|
+
fallback: task,
|
|
359
|
+
}) ?? task;
|
|
351
360
|
const nextLabelIds = [
|
|
352
361
|
...new Set([
|
|
353
362
|
...(taskState.labels ?? []).map((entry) => entry.id),
|
|
@@ -357,7 +366,7 @@ export function useTaskLabelManagement({
|
|
|
357
366
|
|
|
358
367
|
await updateWorkspaceTask(
|
|
359
368
|
workspaceId,
|
|
360
|
-
|
|
369
|
+
canonicalTaskId,
|
|
361
370
|
{
|
|
362
371
|
label_ids: nextLabelIds,
|
|
363
372
|
},
|
|
@@ -368,19 +377,19 @@ export function useTaskLabelManagement({
|
|
|
368
377
|
linkSucceeded = true;
|
|
369
378
|
} catch (linkErr: any) {
|
|
370
379
|
// Rollback on error
|
|
371
|
-
|
|
380
|
+
if (cacheSnapshot) {
|
|
381
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
382
|
+
}
|
|
372
383
|
toast.error(
|
|
373
384
|
'The label was created but could not be attached to the task. Refresh and try manually.'
|
|
374
385
|
);
|
|
375
|
-
if (taskId) {
|
|
376
|
-
queryClient.invalidateQueries({ queryKey: ['task', taskId] });
|
|
377
|
-
}
|
|
378
386
|
console.error('Failed to auto-apply new label', linkErr);
|
|
379
387
|
}
|
|
380
388
|
|
|
381
389
|
// Only show success toast and reset form if link succeeded
|
|
382
390
|
if (linkSucceeded) {
|
|
383
|
-
broadcast?.('task:relations-changed', { taskId:
|
|
391
|
+
broadcast?.('task:relations-changed', { taskId: canonicalTaskId });
|
|
392
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
384
393
|
|
|
385
394
|
// Reset form and close dialog
|
|
386
395
|
setNewLabelName('');
|
|
@@ -6,7 +6,17 @@ import {
|
|
|
6
6
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
7
7
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
8
8
|
import { useState } from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getActiveBoardRefresh,
|
|
11
|
+
useBoardBroadcast,
|
|
12
|
+
} from '../shared/board-broadcast-context';
|
|
13
|
+
import {
|
|
14
|
+
getTaskFromVisibleCaches,
|
|
15
|
+
patchTaskInVisibleCaches,
|
|
16
|
+
restoreTasksFromVisibleCacheSnapshot,
|
|
17
|
+
restoreVisibleTaskCaches,
|
|
18
|
+
snapshotVisibleTaskCaches,
|
|
19
|
+
} from '../shared/task-cache-patches';
|
|
10
20
|
|
|
11
21
|
function getInternalApiOptions() {
|
|
12
22
|
if (typeof window === 'undefined') {
|
|
@@ -89,10 +99,14 @@ export function useTaskProjectManagement({
|
|
|
89
99
|
|
|
90
100
|
// CRITICAL: Get current task state from cache instead of stale prop
|
|
91
101
|
// This ensures we read the most up-to-date state after optimistic updates
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
const canonicalTaskId = taskId ?? task.id;
|
|
103
|
+
const currentTask =
|
|
104
|
+
getTaskFromVisibleCaches({
|
|
105
|
+
queryClient,
|
|
106
|
+
boardId,
|
|
107
|
+
taskId: canonicalTaskId,
|
|
108
|
+
fallback: task,
|
|
109
|
+
}) ?? task;
|
|
96
110
|
|
|
97
111
|
// Check if we're in multi-select mode with multiple tasks selected
|
|
98
112
|
const shouldBulkUpdate =
|
|
@@ -107,11 +121,17 @@ export function useTaskProjectManagement({
|
|
|
107
121
|
|
|
108
122
|
// Cancel any outgoing refetches
|
|
109
123
|
await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
|
|
124
|
+
await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
|
|
110
125
|
|
|
111
126
|
// Snapshot the previous value BEFORE optimistic update
|
|
112
127
|
const previousTasks = queryClient.getQueryData(['tasks', boardId]) as
|
|
113
128
|
| Task[]
|
|
114
129
|
| undefined;
|
|
130
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
131
|
+
queryClient,
|
|
132
|
+
boardId,
|
|
133
|
+
tasksToUpdate
|
|
134
|
+
);
|
|
115
135
|
|
|
116
136
|
// Determine action: remove if ALL selected tasks have the project, add otherwise
|
|
117
137
|
// Use currentTask from cache, not stale task prop
|
|
@@ -133,6 +153,13 @@ export function useTaskProjectManagement({
|
|
|
133
153
|
const fromBoardCache = previousTasks?.find((ct) => ct.id === taskId);
|
|
134
154
|
if (fromBoardCache) return fromBoardCache;
|
|
135
155
|
|
|
156
|
+
const fromVisibleCaches = getTaskFromVisibleCaches({
|
|
157
|
+
queryClient,
|
|
158
|
+
boardId,
|
|
159
|
+
taskId,
|
|
160
|
+
});
|
|
161
|
+
if (fromVisibleCaches) return fromVisibleCaches;
|
|
162
|
+
|
|
136
163
|
// Fallback to individual task cache (for tasks not in board view)
|
|
137
164
|
if (taskId === currentTask.id) return currentTask;
|
|
138
165
|
|
|
@@ -156,54 +183,44 @@ export function useTaskProjectManagement({
|
|
|
156
183
|
|
|
157
184
|
// Get project details from workspace projects for optimistic update
|
|
158
185
|
const project = workspaceProjects.find((p) => p.id === projectId);
|
|
186
|
+
const fallbackProject = project
|
|
187
|
+
? {
|
|
188
|
+
id: project.id,
|
|
189
|
+
name: project.name,
|
|
190
|
+
status: project.status ?? 'unknown',
|
|
191
|
+
}
|
|
192
|
+
: {
|
|
193
|
+
id: projectId,
|
|
194
|
+
name: 'Unknown',
|
|
195
|
+
status: 'unknown',
|
|
196
|
+
};
|
|
159
197
|
|
|
160
198
|
// Optimistically update the cache - only update tasks that actually change
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// CRITICAL: Also update the individual task cache if taskId is provided
|
|
185
|
-
// This ensures the chip menu's task cache stays in sync with the board cache
|
|
186
|
-
if (taskId) {
|
|
187
|
-
queryClient.setQueryData(['task', taskId], (old: Task | undefined) => {
|
|
188
|
-
if (!old) return old;
|
|
189
|
-
if (active && tasksToRemoveFrom.includes(taskId)) {
|
|
190
|
-
// Remove the project
|
|
191
|
-
return {
|
|
192
|
-
...old,
|
|
193
|
-
projects:
|
|
194
|
-
old.projects?.filter((p: any) => p.id !== projectId) || [],
|
|
195
|
-
};
|
|
196
|
-
} else if (!active && tasksNeedingProject.includes(taskId)) {
|
|
197
|
-
// Add the project
|
|
199
|
+
for (const tid of active ? tasksToRemoveFrom : tasksNeedingProject) {
|
|
200
|
+
patchTaskInVisibleCaches({
|
|
201
|
+
queryClient,
|
|
202
|
+
boardId,
|
|
203
|
+
taskId: tid,
|
|
204
|
+
updater: (cachedTask) => {
|
|
205
|
+
if (active) {
|
|
206
|
+
return {
|
|
207
|
+
...cachedTask,
|
|
208
|
+
projects:
|
|
209
|
+
cachedTask.projects?.filter(
|
|
210
|
+
(entry) => entry.id !== projectId
|
|
211
|
+
) || [],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (cachedTask.projects?.some((entry) => entry.id === projectId)) {
|
|
216
|
+
return cachedTask;
|
|
217
|
+
}
|
|
218
|
+
|
|
198
219
|
return {
|
|
199
|
-
...
|
|
200
|
-
projects: [
|
|
201
|
-
...(old.projects || []),
|
|
202
|
-
project || { id: projectId, name: 'Unknown', status: 'unknown' },
|
|
203
|
-
],
|
|
220
|
+
...cachedTask,
|
|
221
|
+
projects: [...(cachedTask.projects || []), fallbackProject],
|
|
204
222
|
};
|
|
205
|
-
}
|
|
206
|
-
return old;
|
|
223
|
+
},
|
|
207
224
|
});
|
|
208
225
|
}
|
|
209
226
|
|
|
@@ -281,34 +298,19 @@ export function useTaskProjectManagement({
|
|
|
281
298
|
(taskId) => !succeededTaskIds.includes(taskId)
|
|
282
299
|
);
|
|
283
300
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (!current) return current;
|
|
290
|
-
return current.map((task) => {
|
|
291
|
-
if (!failedTaskIds.includes(task.id)) {
|
|
292
|
-
return task;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return previousTaskMap.get(task.id) || task;
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
if (taskId && failedTaskIds.includes(taskId)) {
|
|
301
|
-
const previousTask = previousTaskMap.get(taskId);
|
|
302
|
-
if (previousTask) {
|
|
303
|
-
queryClient.setQueryData(['task', taskId], previousTask);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
301
|
+
restoreTasksFromVisibleCacheSnapshot({
|
|
302
|
+
queryClient,
|
|
303
|
+
snapshot: cacheSnapshot,
|
|
304
|
+
taskIds: failedTaskIds,
|
|
305
|
+
});
|
|
307
306
|
|
|
308
307
|
// Broadcast relation changes for all affected tasks
|
|
309
308
|
for (const tid of succeededTaskIds) {
|
|
310
309
|
broadcast?.('task:relations-changed', { taskId: tid });
|
|
311
310
|
}
|
|
311
|
+
if (succeededTaskIds.length > 0) {
|
|
312
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
313
|
+
}
|
|
312
314
|
|
|
313
315
|
if (failedTaskIds.length > 0) {
|
|
314
316
|
toast.warning(
|
|
@@ -329,17 +331,7 @@ export function useTaskProjectManagement({
|
|
|
329
331
|
// Don't auto-clear selection - let user manually clear with "Clear" button
|
|
330
332
|
} catch (e: any) {
|
|
331
333
|
// Rollback on error
|
|
332
|
-
|
|
333
|
-
queryClient.setQueryData(['tasks', boardId], previousTasks);
|
|
334
|
-
if (taskId) {
|
|
335
|
-
const previousTask = previousTasks.find(
|
|
336
|
-
(entry) => entry.id === taskId
|
|
337
|
-
);
|
|
338
|
-
if (previousTask) {
|
|
339
|
-
queryClient.setQueryData(['task', taskId], previousTask);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
334
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
343
335
|
console.error('Failed to toggle project:', e);
|
|
344
336
|
toast.error('Error', {
|
|
345
337
|
description: 'Failed to update project. Please try again.',
|
|
@@ -355,52 +347,55 @@ export function useTaskProjectManagement({
|
|
|
355
347
|
const newProject =
|
|
356
348
|
await createProjectMutation.mutateAsync(newProjectName);
|
|
357
349
|
const canonicalTaskId = taskId ?? task.id;
|
|
350
|
+
const projectForCache = {
|
|
351
|
+
id: newProject.id,
|
|
352
|
+
name: newProject.name,
|
|
353
|
+
status: newProject.status ?? 'unknown',
|
|
354
|
+
};
|
|
358
355
|
|
|
359
356
|
// Auto-apply the newly created project to this task
|
|
360
357
|
let linkSucceeded = false;
|
|
361
|
-
let
|
|
358
|
+
let cacheSnapshot:
|
|
359
|
+
| ReturnType<typeof snapshotVisibleTaskCaches>
|
|
360
|
+
| undefined;
|
|
362
361
|
try {
|
|
363
362
|
// Cancel any outgoing refetches
|
|
364
363
|
await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
|
|
364
|
+
await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
|
|
365
365
|
|
|
366
366
|
// Snapshot the previous value
|
|
367
|
-
|
|
367
|
+
cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
368
|
+
canonicalTaskId,
|
|
369
|
+
]);
|
|
368
370
|
|
|
369
371
|
// Optimistically update the cache
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return t;
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
// CRITICAL: Also update individual task cache if taskId is provided
|
|
387
|
-
if (canonicalTaskId) {
|
|
388
|
-
queryClient.setQueryData(
|
|
389
|
-
['task', canonicalTaskId],
|
|
390
|
-
(old: Task | undefined) => {
|
|
391
|
-
if (!old) return old;
|
|
392
|
-
return {
|
|
393
|
-
...old,
|
|
394
|
-
projects: [...(old.projects || []), newProject],
|
|
395
|
-
};
|
|
372
|
+
patchTaskInVisibleCaches({
|
|
373
|
+
queryClient,
|
|
374
|
+
boardId,
|
|
375
|
+
taskId: canonicalTaskId,
|
|
376
|
+
updater: (cachedTask) => {
|
|
377
|
+
if (
|
|
378
|
+
cachedTask.projects?.some(
|
|
379
|
+
(project) => project.id === newProject.id
|
|
380
|
+
)
|
|
381
|
+
) {
|
|
382
|
+
return cachedTask;
|
|
396
383
|
}
|
|
397
|
-
|
|
398
|
-
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
...cachedTask,
|
|
387
|
+
projects: [...(cachedTask.projects || []), projectForCache],
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
});
|
|
399
391
|
|
|
400
392
|
const taskState =
|
|
401
|
-
(
|
|
402
|
-
|
|
403
|
-
|
|
393
|
+
getTaskFromVisibleCaches({
|
|
394
|
+
queryClient,
|
|
395
|
+
boardId,
|
|
396
|
+
taskId: canonicalTaskId,
|
|
397
|
+
fallback: task,
|
|
398
|
+
}) ?? task;
|
|
404
399
|
const nextProjectIds = [
|
|
405
400
|
...new Set([
|
|
406
401
|
...(taskState.projects ?? []).map((entry) => entry.id),
|
|
@@ -418,21 +413,19 @@ export function useTaskProjectManagement({
|
|
|
418
413
|
);
|
|
419
414
|
linkSucceeded = true;
|
|
420
415
|
} catch (applyErr: any) {
|
|
421
|
-
|
|
416
|
+
if (cacheSnapshot) {
|
|
417
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
418
|
+
}
|
|
422
419
|
toast.error(
|
|
423
420
|
'The project was created but could not be attached to the task. Refresh and try manually.'
|
|
424
421
|
);
|
|
425
|
-
if (canonicalTaskId) {
|
|
426
|
-
queryClient.invalidateQueries({
|
|
427
|
-
queryKey: ['task', canonicalTaskId],
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
422
|
console.error('Failed to auto-apply new project', applyErr);
|
|
431
423
|
}
|
|
432
424
|
|
|
433
425
|
// Only show success toast and reset form if link succeeded
|
|
434
426
|
if (linkSucceeded) {
|
|
435
427
|
broadcast?.('task:relations-changed', { taskId: canonicalTaskId });
|
|
428
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
436
429
|
|
|
437
430
|
// Reset form and close dialog
|
|
438
431
|
setNewProjectName('');
|