@tuturuuu/utils 0.0.3 → 0.6.1

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.
Files changed (186) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +120 -1
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,564 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import {
3
+ moveWorkspaceTask,
4
+ updateWorkspaceTask,
5
+ } from '@tuturuuu/internal-api/tasks';
6
+ import type { Task } from '@tuturuuu/types/primitives/Task';
7
+
8
+ import { getBrowserApiOptions, listAllActiveTasksForList } from './shared';
9
+ import { moveTaskToBoard } from './task-operations';
10
+
11
+ type MoveAllTasksResult = {
12
+ success: true;
13
+ movedCount: number;
14
+ movedTaskIds: string[];
15
+ failedTaskIds: string[];
16
+ };
17
+
18
+ type BulkClearListResult = {
19
+ count: number;
20
+ failedTaskIds: string[];
21
+ };
22
+
23
+ export function useMoveAllTasksFromList(
24
+ currentBoardId: string,
25
+ wsId?: string,
26
+ broadcast?: ((event: string, payload: Record<string, unknown>) => void) | null
27
+ ) {
28
+ const queryClient = useQueryClient();
29
+
30
+ return useMutation({
31
+ mutationFn: async ({
32
+ sourceListId,
33
+ targetListId,
34
+ targetBoardId,
35
+ }: {
36
+ sourceListId: string;
37
+ targetListId: string;
38
+ targetBoardId?: string;
39
+ }) => {
40
+ if (!wsId) {
41
+ throw new Error('Workspace ID is required to move tasks');
42
+ }
43
+
44
+ const tasksToMove = await listAllActiveTasksForList(wsId, sourceListId);
45
+ if (tasksToMove.length === 0) {
46
+ return {
47
+ success: true,
48
+ movedCount: 0,
49
+ movedTaskIds: [] as string[],
50
+ failedTaskIds: [] as string[],
51
+ } satisfies MoveAllTasksResult;
52
+ }
53
+
54
+ const results = [] as {
55
+ status: 'fulfilled' | 'rejected';
56
+ taskId: string;
57
+ }[];
58
+
59
+ for (const task of tasksToMove) {
60
+ try {
61
+ await moveWorkspaceTask(
62
+ wsId,
63
+ task.id,
64
+ {
65
+ list_id: targetListId,
66
+ target_board_id: targetBoardId,
67
+ },
68
+ getBrowserApiOptions()
69
+ );
70
+ results.push({ status: 'fulfilled', taskId: task.id });
71
+ } catch (error) {
72
+ console.error('Failed to move task:', task.id, error);
73
+ results.push({ status: 'rejected', taskId: task.id });
74
+ }
75
+ }
76
+
77
+ const movedTaskIds = results
78
+ .filter((result) => result.status === 'fulfilled')
79
+ .map((result) => result.taskId);
80
+ const failedTaskIds = results
81
+ .filter((result) => result.status === 'rejected')
82
+ .map((result) => result.taskId);
83
+
84
+ if (movedTaskIds.length === 0) {
85
+ throw new Error(
86
+ `Failed to move all ${tasksToMove.length} tasks from list ${sourceListId}`
87
+ );
88
+ }
89
+
90
+ return {
91
+ success: true,
92
+ movedCount: movedTaskIds.length,
93
+ movedTaskIds,
94
+ failedTaskIds,
95
+ } satisfies MoveAllTasksResult;
96
+ },
97
+ onMutate: async ({ sourceListId, targetListId, targetBoardId }) => {
98
+ await queryClient.cancelQueries({ queryKey: ['tasks', currentBoardId] });
99
+ if (targetBoardId && targetBoardId !== currentBoardId) {
100
+ await queryClient.cancelQueries({ queryKey: ['tasks', targetBoardId] });
101
+ }
102
+
103
+ const previousSourceTasks = queryClient.getQueryData([
104
+ 'tasks',
105
+ currentBoardId,
106
+ ]);
107
+ const previousTargetTasks =
108
+ targetBoardId && targetBoardId !== currentBoardId
109
+ ? queryClient.getQueryData(['tasks', targetBoardId])
110
+ : null;
111
+
112
+ const sourceTasks = previousSourceTasks as Task[] | undefined;
113
+ const tasksToMove =
114
+ sourceTasks?.filter((task) => task.list_id === sourceListId) || [];
115
+
116
+ if (tasksToMove.length === 0) {
117
+ return { previousSourceTasks, previousTargetTasks, targetBoardId };
118
+ }
119
+
120
+ if (targetBoardId && targetBoardId !== currentBoardId) {
121
+ queryClient.setQueryData(
122
+ ['tasks', currentBoardId],
123
+ (oldData: Task[] | undefined) => {
124
+ if (!oldData) return oldData;
125
+ return oldData.filter((task) => task.list_id !== sourceListId);
126
+ }
127
+ );
128
+
129
+ queryClient.setQueryData(
130
+ ['tasks', targetBoardId],
131
+ (oldData: Task[] | undefined) => {
132
+ const updatedTasks = tasksToMove.map((task) => ({
133
+ ...task,
134
+ list_id: targetListId,
135
+ }));
136
+
137
+ if (!oldData) return updatedTasks;
138
+
139
+ const filteredOldData = oldData.filter(
140
+ (task) =>
141
+ !tasksToMove.some((movingTask) => movingTask.id === task.id)
142
+ );
143
+ return [...filteredOldData, ...updatedTasks];
144
+ }
145
+ );
146
+ } else {
147
+ queryClient.setQueryData(
148
+ ['tasks', currentBoardId],
149
+ (oldData: Task[] | undefined) => {
150
+ if (!oldData) return oldData;
151
+ return oldData.map((task) =>
152
+ task.list_id === sourceListId
153
+ ? { ...task, list_id: targetListId }
154
+ : task
155
+ );
156
+ }
157
+ );
158
+ }
159
+
160
+ return { previousSourceTasks, previousTargetTasks, targetBoardId };
161
+ },
162
+ onError: (err, _variables, context) => {
163
+ if (context?.previousSourceTasks) {
164
+ queryClient.setQueryData(
165
+ ['tasks', currentBoardId],
166
+ context.previousSourceTasks
167
+ );
168
+ }
169
+ if (
170
+ context?.previousTargetTasks &&
171
+ context.targetBoardId &&
172
+ context.targetBoardId !== currentBoardId
173
+ ) {
174
+ queryClient.setQueryData(
175
+ ['tasks', context.targetBoardId],
176
+ context.previousTargetTasks
177
+ );
178
+ }
179
+
180
+ console.error('Bulk list move failed:', err);
181
+ },
182
+ onSuccess: (data, variables, context) => {
183
+ const movedTaskIds = data.movedTaskIds ?? [];
184
+ const failedTaskIds = new Set(data.failedTaskIds ?? []);
185
+
186
+ if (failedTaskIds.size > 0) {
187
+ if (
188
+ variables.targetBoardId &&
189
+ variables.targetBoardId !== currentBoardId
190
+ ) {
191
+ const previousSourceTasks = context?.previousSourceTasks as
192
+ | Task[]
193
+ | undefined;
194
+ const failedTasks =
195
+ previousSourceTasks?.filter((task) => failedTaskIds.has(task.id)) ??
196
+ [];
197
+
198
+ queryClient.setQueryData(
199
+ ['tasks', currentBoardId],
200
+ (oldData: Task[] | undefined) => {
201
+ const existing = oldData ?? [];
202
+ const preserved = existing.filter(
203
+ (task) => !failedTaskIds.has(task.id)
204
+ );
205
+ return [...preserved, ...failedTasks];
206
+ }
207
+ );
208
+
209
+ queryClient.setQueryData(
210
+ ['tasks', variables.targetBoardId],
211
+ (oldData: Task[] | undefined) => {
212
+ if (!oldData) return oldData;
213
+ return oldData.filter((task) => !failedTaskIds.has(task.id));
214
+ }
215
+ );
216
+ } else {
217
+ queryClient.setQueryData(
218
+ ['tasks', currentBoardId],
219
+ (oldData: Task[] | undefined) => {
220
+ if (!oldData) return oldData;
221
+ return oldData.map((task) =>
222
+ failedTaskIds.has(task.id)
223
+ ? { ...task, list_id: variables.sourceListId }
224
+ : task
225
+ );
226
+ }
227
+ );
228
+ }
229
+ }
230
+
231
+ if (
232
+ variables.targetBoardId &&
233
+ variables.targetBoardId !== currentBoardId
234
+ ) {
235
+ for (const taskId of movedTaskIds) {
236
+ broadcast?.('task:delete', { taskId });
237
+ }
238
+ } else {
239
+ for (const taskId of movedTaskIds) {
240
+ broadcast?.('task:upsert', {
241
+ task: { id: taskId, list_id: variables.targetListId },
242
+ });
243
+ }
244
+ }
245
+ },
246
+ });
247
+ }
248
+
249
+ export async function moveAllTasksFromList(
250
+ wsId: string,
251
+ sourceListId: string,
252
+ targetListId: string,
253
+ targetBoardId?: string
254
+ ) {
255
+ const tasksToMove = await listAllActiveTasksForList(wsId, sourceListId);
256
+
257
+ if (!tasksToMove || tasksToMove.length === 0) {
258
+ return {
259
+ success: true,
260
+ movedCount: 0,
261
+ movedTaskIds: [] as string[],
262
+ failedTaskIds: [] as string[],
263
+ } satisfies MoveAllTasksResult;
264
+ }
265
+
266
+ const results: {
267
+ status: 'fulfilled' | 'rejected';
268
+ value?: { success: boolean; taskId: string };
269
+ reason?: unknown;
270
+ }[] = [];
271
+
272
+ for (const task of tasksToMove) {
273
+ try {
274
+ await moveTaskToBoard(wsId, task.id, targetListId, targetBoardId);
275
+ results.push({
276
+ status: 'fulfilled',
277
+ value: { success: true, taskId: task.id },
278
+ });
279
+ } catch (error) {
280
+ results.push({ status: 'rejected', reason: error });
281
+ }
282
+ }
283
+
284
+ const successful = results.filter(
285
+ (result) => result.status === 'fulfilled' && result.value?.success
286
+ ).length;
287
+
288
+ const failed = results.length - successful;
289
+ const movedTaskIds = results
290
+ .filter((result) => result.status === 'fulfilled' && result.value)
291
+ .map((result) => result.value!.taskId);
292
+ const failedTaskIds = tasksToMove
293
+ .map((task) => task.id)
294
+ .filter((taskId) => !movedTaskIds.includes(taskId));
295
+
296
+ if (successful === 0) {
297
+ throw new Error(`Failed to move ${failed} out of ${results.length} tasks`);
298
+ }
299
+
300
+ return {
301
+ success: true,
302
+ movedCount: successful,
303
+ movedTaskIds,
304
+ failedTaskIds,
305
+ } satisfies MoveAllTasksResult;
306
+ }
307
+
308
+ export function useClearAllAssigneesFromList(boardId: string, wsId?: string) {
309
+ const queryClient = useQueryClient();
310
+
311
+ return useMutation({
312
+ mutationFn: async (listId: string) => {
313
+ if (!wsId) {
314
+ throw new Error('Workspace ID is required to clear assignees');
315
+ }
316
+ const tasks = await listAllActiveTasksForList(wsId, listId);
317
+ let count = 0;
318
+ const failedTaskIds: string[] = [];
319
+
320
+ for (const task of tasks) {
321
+ try {
322
+ await updateWorkspaceTask(
323
+ wsId,
324
+ task.id,
325
+ { assignee_ids: [] },
326
+ getBrowserApiOptions()
327
+ );
328
+ count++;
329
+ } catch (error) {
330
+ console.error(
331
+ `Failed to clear assignees for task ${task.id}:`,
332
+ error
333
+ );
334
+ failedTaskIds.push(task.id);
335
+ }
336
+ }
337
+
338
+ if (tasks.length > 0 && count === 0) {
339
+ throw new Error('Failed to clear any assignees from this list');
340
+ }
341
+
342
+ return { count, failedTaskIds } satisfies BulkClearListResult;
343
+ },
344
+ onMutate: async (listId) => {
345
+ await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
346
+ const previousTasks = queryClient.getQueryData(['tasks', boardId]);
347
+
348
+ queryClient.setQueryData(
349
+ ['tasks', boardId],
350
+ (old: Task[] | undefined) => {
351
+ if (!old) return old;
352
+ return old.map((task) =>
353
+ task.list_id === listId ? { ...task, assignees: [] } : task
354
+ );
355
+ }
356
+ );
357
+
358
+ return { previousTasks };
359
+ },
360
+ onError: (err, _, context) => {
361
+ if (context?.previousTasks) {
362
+ queryClient.setQueryData(['tasks', boardId], context.previousTasks);
363
+ }
364
+ console.error('Failed to clear all assignees:', err);
365
+ },
366
+ onSuccess: (data, listId, context) => {
367
+ if (!context?.previousTasks || data.failedTaskIds.length === 0) {
368
+ return;
369
+ }
370
+
371
+ const previousTasks = context.previousTasks as Task[];
372
+ const failedTaskIds = new Set(data.failedTaskIds);
373
+
374
+ queryClient.setQueryData(
375
+ ['tasks', boardId],
376
+ (old: Task[] | undefined) => {
377
+ if (!old) return old;
378
+
379
+ return old.map((task) => {
380
+ if (!failedTaskIds.has(task.id)) {
381
+ return task;
382
+ }
383
+
384
+ const previousTask = previousTasks.find(
385
+ (entry) => entry.id === task.id && entry.list_id === listId
386
+ );
387
+
388
+ return previousTask ?? task;
389
+ });
390
+ }
391
+ );
392
+ },
393
+ });
394
+ }
395
+
396
+ export function useClearAllLabelsFromList(boardId: string, wsId?: string) {
397
+ const queryClient = useQueryClient();
398
+
399
+ return useMutation({
400
+ mutationFn: async (listId: string) => {
401
+ if (!wsId) {
402
+ throw new Error('Workspace ID is required to clear labels');
403
+ }
404
+ const tasks = await listAllActiveTasksForList(wsId, listId);
405
+ let count = 0;
406
+ const failedTaskIds: string[] = [];
407
+
408
+ for (const task of tasks) {
409
+ try {
410
+ await updateWorkspaceTask(
411
+ wsId,
412
+ task.id,
413
+ { label_ids: [] },
414
+ getBrowserApiOptions()
415
+ );
416
+ count++;
417
+ } catch (error) {
418
+ console.error(`Failed to clear labels for task ${task.id}:`, error);
419
+ failedTaskIds.push(task.id);
420
+ }
421
+ }
422
+
423
+ if (tasks.length > 0 && count === 0) {
424
+ throw new Error('Failed to clear any labels from this list');
425
+ }
426
+
427
+ return { count, failedTaskIds } satisfies BulkClearListResult;
428
+ },
429
+ onMutate: async (listId) => {
430
+ await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
431
+ const previousTasks = queryClient.getQueryData(['tasks', boardId]);
432
+
433
+ queryClient.setQueryData(
434
+ ['tasks', boardId],
435
+ (old: Task[] | undefined) => {
436
+ if (!old) return old;
437
+ return old.map((task) =>
438
+ task.list_id === listId ? { ...task, labels: [] } : task
439
+ );
440
+ }
441
+ );
442
+
443
+ return { previousTasks };
444
+ },
445
+ onError: (err, _, context) => {
446
+ if (context?.previousTasks) {
447
+ queryClient.setQueryData(['tasks', boardId], context.previousTasks);
448
+ }
449
+ console.error('Failed to clear all labels:', err);
450
+ },
451
+ onSuccess: (data, listId, context) => {
452
+ if (!context?.previousTasks || data.failedTaskIds.length === 0) {
453
+ return;
454
+ }
455
+
456
+ const previousTasks = context.previousTasks as Task[];
457
+ const failedTaskIds = new Set(data.failedTaskIds);
458
+
459
+ queryClient.setQueryData(
460
+ ['tasks', boardId],
461
+ (old: Task[] | undefined) => {
462
+ if (!old) return old;
463
+
464
+ return old.map((task) => {
465
+ if (!failedTaskIds.has(task.id)) {
466
+ return task;
467
+ }
468
+
469
+ const previousTask = previousTasks.find(
470
+ (entry) => entry.id === task.id && entry.list_id === listId
471
+ );
472
+
473
+ return previousTask ?? task;
474
+ });
475
+ }
476
+ );
477
+ },
478
+ });
479
+ }
480
+
481
+ export function useClearAllProjectsFromList(boardId: string, wsId?: string) {
482
+ const queryClient = useQueryClient();
483
+
484
+ return useMutation({
485
+ mutationFn: async (listId: string) => {
486
+ if (!wsId) {
487
+ throw new Error('Workspace ID is required to clear projects');
488
+ }
489
+ const tasks = await listAllActiveTasksForList(wsId, listId);
490
+ let count = 0;
491
+ const failedTaskIds: string[] = [];
492
+
493
+ for (const task of tasks) {
494
+ try {
495
+ await updateWorkspaceTask(
496
+ wsId,
497
+ task.id,
498
+ { project_ids: [] },
499
+ getBrowserApiOptions()
500
+ );
501
+ count++;
502
+ } catch (error) {
503
+ console.error(`Failed to clear projects for task ${task.id}:`, error);
504
+ failedTaskIds.push(task.id);
505
+ }
506
+ }
507
+
508
+ if (tasks.length > 0 && count === 0) {
509
+ throw new Error('Failed to clear any projects from this list');
510
+ }
511
+
512
+ return { count, failedTaskIds } satisfies BulkClearListResult;
513
+ },
514
+ onMutate: async (listId) => {
515
+ await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
516
+ const previousTasks = queryClient.getQueryData(['tasks', boardId]);
517
+
518
+ queryClient.setQueryData(
519
+ ['tasks', boardId],
520
+ (old: Task[] | undefined) => {
521
+ if (!old) return old;
522
+ return old.map((task) =>
523
+ task.list_id === listId ? { ...task, projects: [] } : task
524
+ );
525
+ }
526
+ );
527
+
528
+ return { previousTasks };
529
+ },
530
+ onError: (err, _, context) => {
531
+ if (context?.previousTasks) {
532
+ queryClient.setQueryData(['tasks', boardId], context.previousTasks);
533
+ }
534
+ console.error('Failed to clear all projects:', err);
535
+ },
536
+ onSuccess: (data, listId, context) => {
537
+ if (!context?.previousTasks || data.failedTaskIds.length === 0) {
538
+ return;
539
+ }
540
+
541
+ const previousTasks = context.previousTasks as Task[];
542
+ const failedTaskIds = new Set(data.failedTaskIds);
543
+
544
+ queryClient.setQueryData(
545
+ ['tasks', boardId],
546
+ (old: Task[] | undefined) => {
547
+ if (!old) return old;
548
+
549
+ return old.map((task) => {
550
+ if (!failedTaskIds.has(task.id)) {
551
+ return task;
552
+ }
553
+
554
+ const previousTask = previousTasks.find(
555
+ (entry) => entry.id === task.id && entry.list_id === listId
556
+ );
557
+
558
+ return previousTask ?? task;
559
+ });
560
+ }
561
+ );
562
+ },
563
+ });
564
+ }
@@ -0,0 +1,21 @@
1
+ export const PERSONAL_EXTERNAL_STAGING_LIST_ID_PREFIX =
2
+ 'personal-external-staging:';
3
+
4
+ export function getPersonalExternalStagingListId(boardId: string) {
5
+ return `${PERSONAL_EXTERNAL_STAGING_LIST_ID_PREFIX}${boardId}`;
6
+ }
7
+
8
+ export function getPersonalExternalStagingBoardId(listId: string | null) {
9
+ if (
10
+ !listId?.startsWith(PERSONAL_EXTERNAL_STAGING_LIST_ID_PREFIX) ||
11
+ listId.length <= PERSONAL_EXTERNAL_STAGING_LIST_ID_PREFIX.length
12
+ ) {
13
+ return null;
14
+ }
15
+
16
+ return listId.slice(PERSONAL_EXTERNAL_STAGING_LIST_ID_PREFIX.length);
17
+ }
18
+
19
+ export function isPersonalExternalStagingListId(listId: string | null) {
20
+ return getPersonalExternalStagingBoardId(listId) !== null;
21
+ }