@tuturuuu/utils 0.0.3 → 0.6.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 +305 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +120 -1
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- 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
|
+
}
|