@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
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import type { QueryClient, QueryKey } from '@tanstack/react-query';
|
|
2
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
3
|
+
|
|
4
|
+
type WorkspaceTaskCache = {
|
|
5
|
+
task?: Task | null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type MyTasksCache = {
|
|
9
|
+
overdue?: Task[];
|
|
10
|
+
today?: Task[];
|
|
11
|
+
upcoming?: Task[];
|
|
12
|
+
completed?: Task[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type MyCompletedTasksCache = {
|
|
16
|
+
pages?: Array<{
|
|
17
|
+
completed?: Task[];
|
|
18
|
+
}>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type VisibleTaskCacheSnapshot = {
|
|
22
|
+
boardTaskEntries: [QueryKey, Task[] | undefined][];
|
|
23
|
+
taskEntries: [QueryKey, Task | undefined][];
|
|
24
|
+
workspaceTaskEntries: [QueryKey, WorkspaceTaskCache | undefined][];
|
|
25
|
+
myTasksEntries: [QueryKey, MyTasksCache | undefined][];
|
|
26
|
+
myCompletedTaskEntries: [QueryKey, MyCompletedTasksCache | undefined][];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function isBoardTaskQuery(queryKey: QueryKey, boardId: string) {
|
|
30
|
+
return (
|
|
31
|
+
Array.isArray(queryKey) &&
|
|
32
|
+
(queryKey[0] === 'tasks' || queryKey[0] === 'tasks-full') &&
|
|
33
|
+
queryKey[1] === boardId
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isTaskDetailQuery(queryKey: QueryKey, taskIds: Set<string>) {
|
|
38
|
+
return (
|
|
39
|
+
Array.isArray(queryKey) &&
|
|
40
|
+
queryKey[0] === 'task' &&
|
|
41
|
+
typeof queryKey[1] === 'string' &&
|
|
42
|
+
taskIds.has(queryKey[1])
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isWorkspaceTaskQuery(queryKey: QueryKey, taskIds: Set<string>) {
|
|
47
|
+
return (
|
|
48
|
+
Array.isArray(queryKey) &&
|
|
49
|
+
queryKey[0] === 'workspaceTask' &&
|
|
50
|
+
typeof queryKey[2] === 'string' &&
|
|
51
|
+
taskIds.has(queryKey[2])
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function patchTaskArray<T extends { id?: string }>(
|
|
56
|
+
tasks: T[] | undefined,
|
|
57
|
+
taskIds: Set<string>,
|
|
58
|
+
updater: (task: Task) => Task
|
|
59
|
+
): T[] | undefined {
|
|
60
|
+
if (!tasks) return tasks;
|
|
61
|
+
|
|
62
|
+
let changed = false;
|
|
63
|
+
const nextTasks = tasks.map((task) => {
|
|
64
|
+
if (!task.id || !taskIds.has(task.id)) {
|
|
65
|
+
return task;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const updatedTask = updater(task as unknown as Task) as unknown as T;
|
|
69
|
+
if (updatedTask !== task) {
|
|
70
|
+
changed = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return updatedTask;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return changed ? nextTasks : tasks;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function restoreTaskArrayFromSnapshot<T extends { id?: string }>(
|
|
80
|
+
currentTasks: T[] | undefined,
|
|
81
|
+
previousTasks: T[] | undefined,
|
|
82
|
+
taskIds: Set<string>
|
|
83
|
+
): T[] | undefined {
|
|
84
|
+
if (!currentTasks || !previousTasks) return currentTasks;
|
|
85
|
+
|
|
86
|
+
const previousById = new Map(
|
|
87
|
+
previousTasks
|
|
88
|
+
.filter((task) => task.id && taskIds.has(task.id))
|
|
89
|
+
.map((task) => [task.id, task])
|
|
90
|
+
);
|
|
91
|
+
if (previousById.size === 0) return currentTasks;
|
|
92
|
+
|
|
93
|
+
let changed = false;
|
|
94
|
+
const restoredTasks = currentTasks.map((task) => {
|
|
95
|
+
if (!task.id) return task;
|
|
96
|
+
const previousTask = previousById.get(task.id);
|
|
97
|
+
if (!previousTask) return task;
|
|
98
|
+
|
|
99
|
+
changed = true;
|
|
100
|
+
return previousTask;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return changed ? restoredTasks : currentTasks;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function patchMyTasksCache(
|
|
107
|
+
cache: MyTasksCache | undefined,
|
|
108
|
+
taskIds: Set<string>,
|
|
109
|
+
updater: (task: Task) => Task
|
|
110
|
+
) {
|
|
111
|
+
if (!cache) return cache;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...cache,
|
|
115
|
+
overdue: patchTaskArray(cache.overdue, taskIds, updater) ?? cache.overdue,
|
|
116
|
+
today: patchTaskArray(cache.today, taskIds, updater) ?? cache.today,
|
|
117
|
+
upcoming:
|
|
118
|
+
patchTaskArray(cache.upcoming, taskIds, updater) ?? cache.upcoming,
|
|
119
|
+
completed:
|
|
120
|
+
patchTaskArray(cache.completed, taskIds, updater) ?? cache.completed,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function patchMyCompletedTasksCache(
|
|
125
|
+
cache: MyCompletedTasksCache | undefined,
|
|
126
|
+
taskIds: Set<string>,
|
|
127
|
+
updater: (task: Task) => Task
|
|
128
|
+
) {
|
|
129
|
+
if (!cache?.pages) return cache;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
...cache,
|
|
133
|
+
pages: cache.pages.map((page) => ({
|
|
134
|
+
...page,
|
|
135
|
+
completed:
|
|
136
|
+
patchTaskArray(page.completed, taskIds, updater) ?? page.completed,
|
|
137
|
+
})),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function restoreMyTasksCacheFromSnapshot(
|
|
142
|
+
current: MyTasksCache | undefined,
|
|
143
|
+
previous: MyTasksCache | undefined,
|
|
144
|
+
taskIds: Set<string>
|
|
145
|
+
) {
|
|
146
|
+
if (!current || !previous) return current;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
...current,
|
|
150
|
+
overdue:
|
|
151
|
+
restoreTaskArrayFromSnapshot(
|
|
152
|
+
current.overdue,
|
|
153
|
+
previous.overdue,
|
|
154
|
+
taskIds
|
|
155
|
+
) ?? current.overdue,
|
|
156
|
+
today:
|
|
157
|
+
restoreTaskArrayFromSnapshot(current.today, previous.today, taskIds) ??
|
|
158
|
+
current.today,
|
|
159
|
+
upcoming:
|
|
160
|
+
restoreTaskArrayFromSnapshot(
|
|
161
|
+
current.upcoming,
|
|
162
|
+
previous.upcoming,
|
|
163
|
+
taskIds
|
|
164
|
+
) ?? current.upcoming,
|
|
165
|
+
completed:
|
|
166
|
+
restoreTaskArrayFromSnapshot(
|
|
167
|
+
current.completed,
|
|
168
|
+
previous.completed,
|
|
169
|
+
taskIds
|
|
170
|
+
) ?? current.completed,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function restoreMyCompletedTasksCacheFromSnapshot(
|
|
175
|
+
current: MyCompletedTasksCache | undefined,
|
|
176
|
+
previous: MyCompletedTasksCache | undefined,
|
|
177
|
+
taskIds: Set<string>
|
|
178
|
+
) {
|
|
179
|
+
if (!current?.pages || !previous?.pages) return current;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...current,
|
|
183
|
+
pages: current.pages.map((page, index) => ({
|
|
184
|
+
...page,
|
|
185
|
+
completed:
|
|
186
|
+
restoreTaskArrayFromSnapshot(
|
|
187
|
+
page.completed,
|
|
188
|
+
previous.pages?.[index]?.completed,
|
|
189
|
+
taskIds
|
|
190
|
+
) ?? page.completed,
|
|
191
|
+
})),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getTaskFromVisibleCaches({
|
|
196
|
+
queryClient,
|
|
197
|
+
boardId,
|
|
198
|
+
taskId,
|
|
199
|
+
fallback,
|
|
200
|
+
}: {
|
|
201
|
+
queryClient: QueryClient;
|
|
202
|
+
boardId: string;
|
|
203
|
+
taskId: string;
|
|
204
|
+
fallback?: Task;
|
|
205
|
+
}): Task | undefined {
|
|
206
|
+
const taskDetail = queryClient.getQueryData<Task>(['task', taskId]);
|
|
207
|
+
if (taskDetail) return taskDetail;
|
|
208
|
+
|
|
209
|
+
const workspaceTaskEntries = queryClient.getQueriesData<WorkspaceTaskCache>({
|
|
210
|
+
predicate: (query) =>
|
|
211
|
+
isWorkspaceTaskQuery(query.queryKey, new Set([taskId])),
|
|
212
|
+
});
|
|
213
|
+
for (const [, entry] of workspaceTaskEntries) {
|
|
214
|
+
if (entry?.task) return entry.task;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const boardTaskEntries = queryClient.getQueriesData<Task[]>({
|
|
218
|
+
predicate: (query) => isBoardTaskQuery(query.queryKey, boardId),
|
|
219
|
+
});
|
|
220
|
+
for (const [, tasks] of boardTaskEntries) {
|
|
221
|
+
const task = tasks?.find((entry) => entry.id === taskId);
|
|
222
|
+
if (task) return task;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return fallback;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function patchTasksInVisibleCaches({
|
|
229
|
+
queryClient,
|
|
230
|
+
boardId,
|
|
231
|
+
taskIds,
|
|
232
|
+
updater,
|
|
233
|
+
}: {
|
|
234
|
+
queryClient: QueryClient;
|
|
235
|
+
boardId: string;
|
|
236
|
+
taskIds: string[];
|
|
237
|
+
updater: (task: Task) => Task;
|
|
238
|
+
}) {
|
|
239
|
+
const taskIdSet = new Set(taskIds);
|
|
240
|
+
if (taskIdSet.size === 0) return;
|
|
241
|
+
|
|
242
|
+
queryClient.setQueriesData<Task[]>(
|
|
243
|
+
{
|
|
244
|
+
predicate: (query) => isBoardTaskQuery(query.queryKey, boardId),
|
|
245
|
+
},
|
|
246
|
+
(old) => patchTaskArray(old, taskIdSet, updater)
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
queryClient.setQueriesData<Task>(
|
|
250
|
+
{
|
|
251
|
+
predicate: (query) => isTaskDetailQuery(query.queryKey, taskIdSet),
|
|
252
|
+
},
|
|
253
|
+
(old) => (old ? updater(old) : old)
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
queryClient.setQueriesData<WorkspaceTaskCache>(
|
|
257
|
+
{
|
|
258
|
+
predicate: (query) => isWorkspaceTaskQuery(query.queryKey, taskIdSet),
|
|
259
|
+
},
|
|
260
|
+
(old) =>
|
|
261
|
+
old?.task
|
|
262
|
+
? {
|
|
263
|
+
...old,
|
|
264
|
+
task: updater(old.task),
|
|
265
|
+
}
|
|
266
|
+
: old
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
queryClient.setQueriesData<MyTasksCache>({ queryKey: ['my-tasks'] }, (old) =>
|
|
270
|
+
patchMyTasksCache(old, taskIdSet, updater)
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
queryClient.setQueriesData<MyCompletedTasksCache>(
|
|
274
|
+
{ queryKey: ['my-completed-tasks'] },
|
|
275
|
+
(old) => patchMyCompletedTasksCache(old, taskIdSet, updater)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function patchTaskInVisibleCaches({
|
|
280
|
+
queryClient,
|
|
281
|
+
boardId,
|
|
282
|
+
taskId,
|
|
283
|
+
updater,
|
|
284
|
+
}: {
|
|
285
|
+
queryClient: QueryClient;
|
|
286
|
+
boardId: string;
|
|
287
|
+
taskId: string;
|
|
288
|
+
updater: (task: Task) => Task;
|
|
289
|
+
}) {
|
|
290
|
+
patchTasksInVisibleCaches({
|
|
291
|
+
queryClient,
|
|
292
|
+
boardId,
|
|
293
|
+
taskIds: [taskId],
|
|
294
|
+
updater,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function snapshotVisibleTaskCaches(
|
|
299
|
+
queryClient: QueryClient,
|
|
300
|
+
boardId: string,
|
|
301
|
+
taskIds: string[]
|
|
302
|
+
): VisibleTaskCacheSnapshot {
|
|
303
|
+
const taskIdSet = new Set(taskIds);
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
boardTaskEntries: queryClient.getQueriesData<Task[]>({
|
|
307
|
+
predicate: (query) => isBoardTaskQuery(query.queryKey, boardId),
|
|
308
|
+
}),
|
|
309
|
+
taskEntries: queryClient.getQueriesData<Task>({
|
|
310
|
+
predicate: (query) => isTaskDetailQuery(query.queryKey, taskIdSet),
|
|
311
|
+
}),
|
|
312
|
+
workspaceTaskEntries: queryClient.getQueriesData<WorkspaceTaskCache>({
|
|
313
|
+
predicate: (query) => isWorkspaceTaskQuery(query.queryKey, taskIdSet),
|
|
314
|
+
}),
|
|
315
|
+
myTasksEntries: queryClient.getQueriesData<MyTasksCache>({
|
|
316
|
+
queryKey: ['my-tasks'],
|
|
317
|
+
}),
|
|
318
|
+
myCompletedTaskEntries: queryClient.getQueriesData<MyCompletedTasksCache>({
|
|
319
|
+
queryKey: ['my-completed-tasks'],
|
|
320
|
+
}),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function restoreVisibleTaskCaches(
|
|
325
|
+
queryClient: QueryClient,
|
|
326
|
+
snapshot: VisibleTaskCacheSnapshot
|
|
327
|
+
) {
|
|
328
|
+
for (const [queryKey, data] of snapshot.boardTaskEntries) {
|
|
329
|
+
queryClient.setQueryData(queryKey, data);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const [queryKey, data] of snapshot.taskEntries) {
|
|
333
|
+
queryClient.setQueryData(queryKey, data);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const [queryKey, data] of snapshot.workspaceTaskEntries) {
|
|
337
|
+
queryClient.setQueryData(queryKey, data);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const [queryKey, data] of snapshot.myTasksEntries) {
|
|
341
|
+
queryClient.setQueryData(queryKey, data);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const [queryKey, data] of snapshot.myCompletedTaskEntries) {
|
|
345
|
+
queryClient.setQueryData(queryKey, data);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function restoreTasksFromVisibleCacheSnapshot({
|
|
350
|
+
queryClient,
|
|
351
|
+
snapshot,
|
|
352
|
+
taskIds,
|
|
353
|
+
}: {
|
|
354
|
+
queryClient: QueryClient;
|
|
355
|
+
snapshot: VisibleTaskCacheSnapshot;
|
|
356
|
+
taskIds: string[];
|
|
357
|
+
}) {
|
|
358
|
+
const taskIdSet = new Set(taskIds);
|
|
359
|
+
if (taskIdSet.size === 0) return;
|
|
360
|
+
|
|
361
|
+
for (const [queryKey, previousTasks] of snapshot.boardTaskEntries) {
|
|
362
|
+
queryClient.setQueryData<Task[]>(queryKey, (currentTasks) =>
|
|
363
|
+
restoreTaskArrayFromSnapshot(currentTasks, previousTasks, taskIdSet)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const [queryKey, previousTask] of snapshot.taskEntries) {
|
|
368
|
+
if (!Array.isArray(queryKey) || typeof queryKey[1] !== 'string') continue;
|
|
369
|
+
if (!taskIdSet.has(queryKey[1])) continue;
|
|
370
|
+
queryClient.setQueryData(queryKey, previousTask);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const [queryKey, previousEntry] of snapshot.workspaceTaskEntries) {
|
|
374
|
+
if (!Array.isArray(queryKey) || typeof queryKey[2] !== 'string') continue;
|
|
375
|
+
if (!taskIdSet.has(queryKey[2])) continue;
|
|
376
|
+
queryClient.setQueryData(queryKey, previousEntry);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const [queryKey, previousEntry] of snapshot.myTasksEntries) {
|
|
380
|
+
queryClient.setQueryData<MyTasksCache>(queryKey, (currentEntry) =>
|
|
381
|
+
restoreMyTasksCacheFromSnapshot(currentEntry, previousEntry, taskIdSet)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const [queryKey, previousEntry] of snapshot.myCompletedTaskEntries) {
|
|
386
|
+
queryClient.setQueryData<MyCompletedTasksCache>(queryKey, (currentEntry) =>
|
|
387
|
+
restoreMyCompletedTasksCacheFromSnapshot(
|
|
388
|
+
currentEntry,
|
|
389
|
+
previousEntry,
|
|
390
|
+
taskIdSet
|
|
391
|
+
)
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -189,6 +189,8 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
189
189
|
taskWsId: state.taskWsId,
|
|
190
190
|
taskWorkspacePersonal: state.taskWorkspacePersonal,
|
|
191
191
|
taskWorkspaceTier: state.taskWorkspaceTier,
|
|
192
|
+
canUseBoardAssignees: state.canUseBoardAssignees,
|
|
193
|
+
assigneeMemberSource: state.assigneeMemberSource,
|
|
192
194
|
});
|
|
193
195
|
return;
|
|
194
196
|
}
|
|
@@ -210,6 +212,8 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
210
212
|
taskWsId: currentTaskWsId,
|
|
211
213
|
taskWorkspacePersonal: state.taskWorkspacePersonal,
|
|
212
214
|
taskWorkspaceTier: state.taskWorkspaceTier,
|
|
215
|
+
canUseBoardAssignees: state.canUseBoardAssignees,
|
|
216
|
+
assigneeMemberSource: state.assigneeMemberSource,
|
|
213
217
|
});
|
|
214
218
|
return;
|
|
215
219
|
}
|
|
@@ -223,8 +227,10 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
223
227
|
openTask,
|
|
224
228
|
openTaskById,
|
|
225
229
|
queryClient,
|
|
230
|
+
state.assigneeMemberSource,
|
|
226
231
|
state.availableLists,
|
|
227
232
|
state.boardId,
|
|
233
|
+
state.canUseBoardAssignees,
|
|
228
234
|
state.task?.id,
|
|
229
235
|
state.taskWorkspacePersonal,
|
|
230
236
|
state.taskWorkspaceTier,
|
|
@@ -238,9 +244,17 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
238
244
|
return openTaskById(taskId, {
|
|
239
245
|
taskWsId: wsId,
|
|
240
246
|
taskWorkspacePersonal: isPersonalWorkspace,
|
|
247
|
+
canUseBoardAssignees: state.canUseBoardAssignees,
|
|
248
|
+
assigneeMemberSource: state.assigneeMemberSource,
|
|
241
249
|
});
|
|
242
250
|
},
|
|
243
|
-
[
|
|
251
|
+
[
|
|
252
|
+
isPersonalWorkspace,
|
|
253
|
+
openTaskById,
|
|
254
|
+
state.assigneeMemberSource,
|
|
255
|
+
state.canUseBoardAssignees,
|
|
256
|
+
wsId,
|
|
257
|
+
]
|
|
244
258
|
);
|
|
245
259
|
|
|
246
260
|
useEffect(() => {
|
|
@@ -388,10 +402,14 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
388
402
|
taskWsId: state.taskWsId,
|
|
389
403
|
taskWorkspacePersonal: state.taskWorkspacePersonal,
|
|
390
404
|
taskWorkspaceTier: state.taskWorkspaceTier,
|
|
405
|
+
canUseBoardAssignees: state.canUseBoardAssignees,
|
|
406
|
+
assigneeMemberSource: state.assigneeMemberSource,
|
|
391
407
|
initialSharedContext: state.initialSharedContext,
|
|
392
408
|
});
|
|
393
409
|
}, [
|
|
394
410
|
openTaskById,
|
|
411
|
+
state.assigneeMemberSource,
|
|
412
|
+
state.canUseBoardAssignees,
|
|
395
413
|
state.initialSharedContext,
|
|
396
414
|
state.availableLists,
|
|
397
415
|
state.boardId,
|
|
@@ -539,6 +557,8 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
539
557
|
taskLoadError={state.taskLoadError}
|
|
540
558
|
taskHydrationVersion={state.taskHydrationVersion}
|
|
541
559
|
isPersonalWorkspace={isPersonalWorkspace}
|
|
560
|
+
canUseBoardAssignees={state.canUseBoardAssignees}
|
|
561
|
+
assigneeMemberSource={state.assigneeMemberSource}
|
|
542
562
|
parentTaskId={state.parentTaskId}
|
|
543
563
|
parentTaskName={state.parentTaskName}
|
|
544
564
|
pendingRelationship={state.pendingRelationship}
|
package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx
CHANGED
|
@@ -28,6 +28,8 @@ function updateBodyFadeAttribute(enabled: boolean) {
|
|
|
28
28
|
interface QuickSettingsPopoverProps {
|
|
29
29
|
/** Whether the workspace is personal (forces auto-assign to true) */
|
|
30
30
|
isPersonalWorkspace?: boolean;
|
|
31
|
+
open?: boolean;
|
|
32
|
+
onOpenChange?: (open: boolean) => void;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -36,6 +38,8 @@ interface QuickSettingsPopoverProps {
|
|
|
36
38
|
*/
|
|
37
39
|
export function QuickSettingsPopover({
|
|
38
40
|
isPersonalWorkspace = false,
|
|
41
|
+
open,
|
|
42
|
+
onOpenChange,
|
|
39
43
|
}: QuickSettingsPopoverProps) {
|
|
40
44
|
const t = useTranslations('settings.tasks');
|
|
41
45
|
const tCommon = useTranslations('common');
|
|
@@ -96,7 +100,7 @@ export function QuickSettingsPopover({
|
|
|
96
100
|
: (settings?.task_auto_assign_to_self ?? false);
|
|
97
101
|
|
|
98
102
|
return (
|
|
99
|
-
<Popover>
|
|
103
|
+
<Popover open={open} onOpenChange={onOpenChange}>
|
|
100
104
|
<Tooltip>
|
|
101
105
|
<TooltipTrigger asChild>
|
|
102
106
|
<PopoverTrigger asChild>
|
|
@@ -16,7 +16,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
|
|
|
16
16
|
import { cn } from '@tuturuuu/utils/format';
|
|
17
17
|
import { getTicketIdentifier } from '@tuturuuu/utils/task-helper';
|
|
18
18
|
import { useTranslations } from 'next-intl';
|
|
19
|
-
import type
|
|
19
|
+
import { type ReactNode, useState } from 'react';
|
|
20
20
|
import { TaskViewerAvatarsComponent } from '../../user-presence-avatars';
|
|
21
21
|
import { TaskDialogActions } from '../task-dialog-actions';
|
|
22
22
|
import type {
|
|
@@ -268,6 +268,19 @@ export function TaskDialogHeader({
|
|
|
268
268
|
onScrollToUserCursor,
|
|
269
269
|
}: TaskDialogHeaderProps) {
|
|
270
270
|
const t = useTranslations();
|
|
271
|
+
const [activeHeaderOverlay, setActiveHeaderOverlay] = useState<
|
|
272
|
+
'quick-settings' | 'more-menu' | null
|
|
273
|
+
>(null);
|
|
274
|
+
|
|
275
|
+
const setHeaderOverlayOpen = (
|
|
276
|
+
overlay: 'quick-settings' | 'more-menu',
|
|
277
|
+
open: boolean
|
|
278
|
+
) => {
|
|
279
|
+
setActiveHeaderOverlay((currentOverlay) => {
|
|
280
|
+
if (open) return overlay;
|
|
281
|
+
return currentOverlay === overlay ? null : currentOverlay;
|
|
282
|
+
});
|
|
283
|
+
};
|
|
271
284
|
|
|
272
285
|
// Use custom headerInfo if provided, otherwise generate from task context
|
|
273
286
|
const resolvedHeaderInfo =
|
|
@@ -452,7 +465,13 @@ export function TaskDialogHeader({
|
|
|
452
465
|
|
|
453
466
|
{/* Quick Settings */}
|
|
454
467
|
{!controlsDisabled && (
|
|
455
|
-
<QuickSettingsPopover
|
|
468
|
+
<QuickSettingsPopover
|
|
469
|
+
isPersonalWorkspace={isPersonalWorkspace}
|
|
470
|
+
open={activeHeaderOverlay === 'quick-settings'}
|
|
471
|
+
onOpenChange={(open) =>
|
|
472
|
+
setHeaderOverlayOpen('quick-settings', open)
|
|
473
|
+
}
|
|
474
|
+
/>
|
|
456
475
|
)}
|
|
457
476
|
|
|
458
477
|
<TaskDialogActions
|
|
@@ -471,6 +490,10 @@ export function TaskDialogHeader({
|
|
|
471
490
|
onOpenShareDialog={onOpenShareDialog}
|
|
472
491
|
disabled={disabled}
|
|
473
492
|
controlsDisabled={controlsDisabled}
|
|
493
|
+
moreMenuOpen={activeHeaderOverlay === 'more-menu'}
|
|
494
|
+
onMoreMenuOpenChange={(open) =>
|
|
495
|
+
setHeaderOverlayOpen('more-menu', open)
|
|
496
|
+
}
|
|
474
497
|
/>
|
|
475
498
|
|
|
476
499
|
{/* Hide save button in edit mode when realtime is enabled (either cursors or Yjs sync) */}
|
|
@@ -21,6 +21,8 @@ interface TaskListSelectorProps {
|
|
|
21
21
|
availableLists: TaskList[];
|
|
22
22
|
disabled?: boolean;
|
|
23
23
|
compact?: boolean;
|
|
24
|
+
open?: boolean;
|
|
25
|
+
onOpenChange?: (open: boolean) => void;
|
|
24
26
|
onListChange: (listId: string) => void;
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -31,11 +33,15 @@ export function TaskListSelector({
|
|
|
31
33
|
availableLists,
|
|
32
34
|
disabled = false,
|
|
33
35
|
compact = false,
|
|
36
|
+
open,
|
|
37
|
+
onOpenChange,
|
|
34
38
|
onListChange,
|
|
35
39
|
}: TaskListSelectorProps) {
|
|
36
40
|
const t = useTranslations();
|
|
37
|
-
const [
|
|
41
|
+
const [uncontrolledPopoverOpen, setUncontrolledPopoverOpen] = useState(false);
|
|
38
42
|
const [isCreateListDialogOpen, setIsCreateListDialogOpen] = useState(false);
|
|
43
|
+
const isPopoverOpen = open ?? uncontrolledPopoverOpen;
|
|
44
|
+
const setIsPopoverOpen = onOpenChange ?? setUncontrolledPopoverOpen;
|
|
39
45
|
|
|
40
46
|
const statusLabels = useMemo(
|
|
41
47
|
() => ({
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
listWorkspaceTaskBoardViewableMembers,
|
|
4
|
+
listWorkspaceTaskProjects,
|
|
5
|
+
} from '@tuturuuu/internal-api/tasks';
|
|
3
6
|
import { createClient } from '@tuturuuu/supabase/next/client';
|
|
4
7
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
5
8
|
import {
|
|
@@ -89,10 +92,19 @@ interface UseTaskDataProps {
|
|
|
89
92
|
isOpen: boolean;
|
|
90
93
|
propAvailableLists?: TaskList[];
|
|
91
94
|
taskSearchQuery?: string;
|
|
95
|
+
canUseBoardAssignees?: boolean;
|
|
96
|
+
assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
|
|
92
97
|
/** Pre-loaded data for shared task context - bypasses internal fetches when provided */
|
|
93
98
|
sharedContext?: SharedTaskContext;
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
type TaskDialogWorkspaceMember = {
|
|
102
|
+
id: string;
|
|
103
|
+
user_id: string;
|
|
104
|
+
display_name: string;
|
|
105
|
+
avatar_url?: string | null;
|
|
106
|
+
};
|
|
107
|
+
|
|
96
108
|
// UUID validation regex
|
|
97
109
|
const UUID_REGEX =
|
|
98
110
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -107,6 +119,8 @@ export function useTaskData({
|
|
|
107
119
|
isOpen,
|
|
108
120
|
propAvailableLists,
|
|
109
121
|
taskSearchQuery = '',
|
|
122
|
+
canUseBoardAssignees,
|
|
123
|
+
assigneeMemberSource,
|
|
110
124
|
sharedContext,
|
|
111
125
|
}: UseTaskDataProps) {
|
|
112
126
|
// If sharedContext is provided, use pre-loaded data and skip fetches
|
|
@@ -174,17 +188,19 @@ export function useTaskData({
|
|
|
174
188
|
);
|
|
175
189
|
const workspaceLabels =
|
|
176
190
|
sharedContext?.workspaceLabels || fetchedWorkspaceLabels;
|
|
191
|
+
const effectiveAssigneeMemberSource = assigneeMemberSource ?? 'workspace';
|
|
192
|
+
const shouldLoadWorkspaceMembers =
|
|
193
|
+
canUseBoardAssignees !== false && effectiveAssigneeMemberSource !== 'board';
|
|
194
|
+
const shouldLoadBoardViewableMembers =
|
|
195
|
+
canUseBoardAssignees !== false &&
|
|
196
|
+
effectiveAssigneeMemberSource !== 'workspace' &&
|
|
197
|
+
!!boardId;
|
|
177
198
|
|
|
178
199
|
// Workspace members
|
|
179
200
|
const { data: fetchedWorkspaceMembers = [] } = useQuery<
|
|
180
|
-
|
|
181
|
-
id: string;
|
|
182
|
-
user_id: string;
|
|
183
|
-
display_name: string;
|
|
184
|
-
avatar_url?: string | null;
|
|
185
|
-
}>
|
|
201
|
+
TaskDialogWorkspaceMember[]
|
|
186
202
|
>({
|
|
187
|
-
queryKey: ['workspace-members', realWorkspaceId],
|
|
203
|
+
queryKey: ['workspace-members', realWorkspaceId, 'workspace'],
|
|
188
204
|
queryFn: async () => {
|
|
189
205
|
if (!realWorkspaceId || !isValidWsId) return [];
|
|
190
206
|
|
|
@@ -234,11 +250,64 @@ export function useTaskData({
|
|
|
234
250
|
(a.display_name || '').localeCompare(b.display_name || '')
|
|
235
251
|
);
|
|
236
252
|
},
|
|
237
|
-
enabled:
|
|
253
|
+
enabled:
|
|
254
|
+
!!realWorkspaceId &&
|
|
255
|
+
isOpen &&
|
|
256
|
+
isValidWsId &&
|
|
257
|
+
!hasSharedContext &&
|
|
258
|
+
shouldLoadWorkspaceMembers,
|
|
259
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
260
|
+
});
|
|
261
|
+
const { data: fetchedBoardViewableMembers = [] } = useQuery<
|
|
262
|
+
TaskDialogWorkspaceMember[]
|
|
263
|
+
>({
|
|
264
|
+
queryKey: ['task-board-viewable-members', realWorkspaceId, boardId],
|
|
265
|
+
queryFn: async () => {
|
|
266
|
+
if (!realWorkspaceId || !isValidWsId || !boardId) return [];
|
|
267
|
+
|
|
268
|
+
const payload = await listWorkspaceTaskBoardViewableMembers(
|
|
269
|
+
realWorkspaceId,
|
|
270
|
+
boardId
|
|
271
|
+
);
|
|
272
|
+
const members = Array.isArray(payload?.members) ? payload.members : [];
|
|
273
|
+
|
|
274
|
+
return members
|
|
275
|
+
.map((member) => ({
|
|
276
|
+
id: member.user_id,
|
|
277
|
+
user_id: member.user_id,
|
|
278
|
+
display_name: member.display_name || member.email || member.user_id,
|
|
279
|
+
avatar_url: member.avatar_url,
|
|
280
|
+
}))
|
|
281
|
+
.sort((a, b) =>
|
|
282
|
+
(a.display_name || '').localeCompare(b.display_name || '')
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
enabled:
|
|
286
|
+
!!realWorkspaceId &&
|
|
287
|
+
isOpen &&
|
|
288
|
+
isValidWsId &&
|
|
289
|
+
!hasSharedContext &&
|
|
290
|
+
shouldLoadBoardViewableMembers,
|
|
238
291
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
239
292
|
});
|
|
293
|
+
const mergedFetchedWorkspaceMembers = useMemo(() => {
|
|
294
|
+
const seen = new Set<string>();
|
|
295
|
+
const merged: TaskDialogWorkspaceMember[] = [];
|
|
296
|
+
|
|
297
|
+
for (const member of [
|
|
298
|
+
...fetchedWorkspaceMembers,
|
|
299
|
+
...fetchedBoardViewableMembers,
|
|
300
|
+
]) {
|
|
301
|
+
const memberId = member.user_id || member.id;
|
|
302
|
+
if (!memberId || seen.has(memberId)) continue;
|
|
303
|
+
seen.add(memberId);
|
|
304
|
+
merged.push(member);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return merged;
|
|
308
|
+
}, [fetchedBoardViewableMembers, fetchedWorkspaceMembers]);
|
|
240
309
|
const workspaceMembers =
|
|
241
|
-
sharedContext?.workspaceMembers ||
|
|
310
|
+
sharedContext?.workspaceMembers || mergedFetchedWorkspaceMembers;
|
|
242
311
|
|
|
243
312
|
// Task projects
|
|
244
313
|
const { data: fetchedTaskProjects = [] } = useQuery({
|