@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.
Files changed (186) hide show
  1. package/CHANGELOG.md +305 -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,264 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { moveWorkspaceTask } from '@tuturuuu/internal-api/tasks';
3
+ import type { Task } from '@tuturuuu/types/primitives/Task';
4
+ import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
5
+ import { isTaskBoardCompletedStatus } from '../task-list-status';
6
+
7
+ import { getBrowserApiOptions } from './shared';
8
+ import { moveTask } from './task-operations';
9
+
10
+ function applyOptimisticMoveToList(
11
+ task: Task,
12
+ newListId: string,
13
+ list?: TaskList
14
+ ): Task {
15
+ const now = new Date().toISOString();
16
+ const targetIsCompleted = isTaskBoardCompletedStatus(list?.status);
17
+ const targetIsClosed = list?.status === 'closed';
18
+
19
+ return {
20
+ ...task,
21
+ list_id: newListId,
22
+ completed: targetIsCompleted,
23
+ completed_at: targetIsCompleted ? (task.completed_at ?? now) : null,
24
+ closed_at: targetIsClosed ? (task.closed_at ?? now) : null,
25
+ } as Task;
26
+ }
27
+
28
+ export function useMoveTask(boardId: string, wsId?: string) {
29
+ const queryClient = useQueryClient();
30
+
31
+ return useMutation({
32
+ mutationFn: async ({
33
+ taskId,
34
+ newListId,
35
+ }: {
36
+ taskId: string;
37
+ newListId: string;
38
+ }) => {
39
+ if (!wsId) {
40
+ console.error('Workspace ID missing for moveTask');
41
+ throw new Error('Workspace ID is required to move tasks');
42
+ }
43
+
44
+ const baseUrl =
45
+ typeof window !== 'undefined' ? window.location.origin : undefined;
46
+ const result = await moveTask(wsId, taskId, newListId, {
47
+ baseUrl: baseUrl ?? undefined,
48
+ });
49
+
50
+ return result;
51
+ },
52
+ onMutate: async ({ taskId, newListId }) => {
53
+ await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
54
+
55
+ const previousTasks = queryClient.getQueryData(['tasks', boardId]);
56
+
57
+ queryClient.setQueryData(
58
+ ['tasks', boardId],
59
+ (old: Task[] | undefined) => {
60
+ if (!old) return old;
61
+ return old.map((task) => {
62
+ if (task.id === taskId) {
63
+ const targetList = queryClient.getQueryData([
64
+ 'task_lists',
65
+ boardId,
66
+ ]) as TaskList[] | undefined;
67
+ const list = targetList?.find((l) => l.id === newListId);
68
+ return applyOptimisticMoveToList(task, newListId, list);
69
+ }
70
+ return task;
71
+ });
72
+ }
73
+ );
74
+
75
+ return { previousTasks };
76
+ },
77
+ onError: (err, _variables, context) => {
78
+ if (context?.previousTasks) {
79
+ queryClient.setQueryData(['tasks', boardId], context.previousTasks);
80
+ }
81
+
82
+ console.error('Failed to move task:', err);
83
+ },
84
+ onSuccess: (updatedTask) => {
85
+ queryClient.setQueryData(
86
+ ['tasks', boardId],
87
+ (old: Task[] | undefined) => {
88
+ if (!old) return old;
89
+ return old.map((task) =>
90
+ task.id === updatedTask.id ? updatedTask : task
91
+ );
92
+ }
93
+ );
94
+ },
95
+ });
96
+ }
97
+
98
+ export function useMoveTaskToBoard(currentBoardId: string, wsId?: string) {
99
+ const queryClient = useQueryClient();
100
+
101
+ return useMutation({
102
+ mutationFn: async ({
103
+ taskId,
104
+ newListId,
105
+ targetBoardId,
106
+ }: {
107
+ taskId: string;
108
+ newListId: string;
109
+ targetBoardId?: string;
110
+ }) => {
111
+ if (!wsId) {
112
+ throw new Error('Workspace ID is required to move tasks');
113
+ }
114
+
115
+ const result = await moveWorkspaceTask(
116
+ wsId,
117
+ taskId,
118
+ {
119
+ list_id: newListId,
120
+ target_board_id: targetBoardId,
121
+ },
122
+ getBrowserApiOptions()
123
+ );
124
+
125
+ return result;
126
+ },
127
+ onMutate: async ({ taskId, newListId, targetBoardId }) => {
128
+ await queryClient.cancelQueries({ queryKey: ['tasks', currentBoardId] });
129
+ if (targetBoardId && targetBoardId !== currentBoardId) {
130
+ await queryClient.cancelQueries({ queryKey: ['tasks', targetBoardId] });
131
+ }
132
+
133
+ const previousCurrentBoardTasks = queryClient.getQueryData([
134
+ 'tasks',
135
+ currentBoardId,
136
+ ]);
137
+ const previousTargetBoardTasks =
138
+ targetBoardId && targetBoardId !== currentBoardId
139
+ ? queryClient.getQueryData(['tasks', targetBoardId])
140
+ : null;
141
+
142
+ if (targetBoardId && targetBoardId !== currentBoardId) {
143
+ queryClient.setQueryData(
144
+ ['tasks', currentBoardId],
145
+ (old: Task[] | undefined) => {
146
+ if (!old) return old;
147
+ return old.filter((task) => task.id !== taskId);
148
+ }
149
+ );
150
+
151
+ queryClient.setQueryData(
152
+ ['tasks', targetBoardId],
153
+ (old: Task[] | undefined) => {
154
+ if (!old) return old;
155
+
156
+ const currentBoardTasks = previousCurrentBoardTasks as
157
+ | Task[]
158
+ | undefined;
159
+ const taskToMove = currentBoardTasks?.find((t) => t.id === taskId);
160
+
161
+ if (!taskToMove) return old;
162
+
163
+ const targetList = queryClient.getQueryData([
164
+ 'task_lists',
165
+ targetBoardId,
166
+ ]) as TaskList[] | undefined;
167
+ const list = targetList?.find((l) => l.id === newListId);
168
+
169
+ const updatedTask = applyOptimisticMoveToList(
170
+ taskToMove,
171
+ newListId,
172
+ list
173
+ );
174
+
175
+ return [...old, updatedTask];
176
+ }
177
+ );
178
+ } else {
179
+ queryClient.setQueryData(
180
+ ['tasks', currentBoardId],
181
+ (old: Task[] | undefined) => {
182
+ if (!old) return old;
183
+ return old.map((task) => {
184
+ if (task.id === taskId) {
185
+ const targetList = queryClient.getQueryData([
186
+ 'task_lists',
187
+ currentBoardId,
188
+ ]) as TaskList[] | undefined;
189
+ const list = targetList?.find((l) => l.id === newListId);
190
+ return applyOptimisticMoveToList(task, newListId, list);
191
+ }
192
+ return task;
193
+ });
194
+ }
195
+ );
196
+ }
197
+
198
+ return {
199
+ previousCurrentBoardTasks,
200
+ previousTargetBoardTasks,
201
+ targetBoardId: targetBoardId || currentBoardId,
202
+ };
203
+ },
204
+ onError: (err, _variables, context) => {
205
+ if (context?.previousCurrentBoardTasks) {
206
+ queryClient.setQueryData(
207
+ ['tasks', currentBoardId],
208
+ context.previousCurrentBoardTasks
209
+ );
210
+ }
211
+
212
+ if (
213
+ context?.previousTargetBoardTasks &&
214
+ context.targetBoardId !== currentBoardId
215
+ ) {
216
+ queryClient.setQueryData(
217
+ ['tasks', context.targetBoardId],
218
+ context.previousTargetBoardTasks
219
+ );
220
+ }
221
+
222
+ console.error('Failed to move task to board:', err);
223
+ },
224
+ onSuccess: (result) => {
225
+ if (result.movedToDifferentBoard) {
226
+ queryClient.setQueryData(
227
+ ['tasks', result.sourceBoardId],
228
+ (old: Task[] | undefined) => {
229
+ if (!old) return old;
230
+ return old.filter((task) => task.id !== result.task.id);
231
+ }
232
+ );
233
+
234
+ queryClient.setQueryData(
235
+ ['tasks', result.targetBoardId],
236
+ (old: Task[] | undefined) => {
237
+ if (!old) return [result.task];
238
+
239
+ const existingIndex = old.findIndex(
240
+ (task) => task.id === result.task.id
241
+ );
242
+ if (existingIndex >= 0) {
243
+ const updated = [...old];
244
+ updated[existingIndex] = result.task;
245
+ return updated;
246
+ } else {
247
+ return [...old, result.task];
248
+ }
249
+ }
250
+ );
251
+ } else {
252
+ queryClient.setQueryData(
253
+ ['tasks', currentBoardId],
254
+ (old: Task[] | undefined) => {
255
+ if (!old) return old;
256
+ return old.map((task) =>
257
+ task.id === result.task.id ? result.task : task
258
+ );
259
+ }
260
+ );
261
+ }
262
+ },
263
+ });
264
+ }
@@ -0,0 +1,278 @@
1
+ import type { QueryClient } from '@tanstack/react-query';
2
+ import type { InternalApiClientOptions } from '@tuturuuu/internal-api/client';
3
+ import {
4
+ createWorkspaceTask,
5
+ getWorkspaceTask,
6
+ listWorkspaceBoardsWithLists,
7
+ moveWorkspaceTask,
8
+ triggerWorkspaceTaskEmbedding,
9
+ updateWorkspaceTask,
10
+ } from '@tuturuuu/internal-api/tasks';
11
+ import type { Task } from '@tuturuuu/types/primitives/Task';
12
+ import { isTaskBoardCompletedStatus } from '../task-list-status';
13
+
14
+ import {
15
+ getBrowserApiOptions,
16
+ getMutationApiOptions,
17
+ toWorkspaceTaskUpdatePayload,
18
+ } from './shared';
19
+
20
+ export async function getTaskAssignees(wsId: string, taskId: string) {
21
+ const { task } = await getWorkspaceTask(wsId, taskId, getBrowserApiOptions());
22
+ return task.assignees ?? [];
23
+ }
24
+
25
+ function buildTaskArchivedStatusUpdates(
26
+ task: {
27
+ completed?: boolean | null;
28
+ completed_at?: string | null;
29
+ closed_at?: string | null;
30
+ },
31
+ listStatus: string | null | undefined
32
+ ) {
33
+ const updates: {
34
+ completed?: boolean;
35
+ completed_at?: string | null;
36
+ closed_at?: string | null;
37
+ } = {};
38
+
39
+ if (isTaskBoardCompletedStatus(listStatus)) {
40
+ if (task.completed !== true || !task.completed_at || task.closed_at) {
41
+ updates.completed = true;
42
+ updates.completed_at = task.completed_at ?? new Date().toISOString();
43
+ updates.closed_at = null;
44
+ }
45
+ } else if (listStatus === 'closed') {
46
+ if (task.completed !== false || !task.closed_at || task.completed_at) {
47
+ updates.completed = false;
48
+ updates.closed_at = new Date().toISOString();
49
+ updates.completed_at = null;
50
+ }
51
+ } else {
52
+ if (task.completed !== false) {
53
+ updates.completed = false;
54
+ }
55
+ if (task.completed_at) {
56
+ updates.completed_at = null;
57
+ }
58
+ if (task.closed_at) {
59
+ updates.closed_at = null;
60
+ }
61
+ }
62
+
63
+ return updates;
64
+ }
65
+
66
+ export async function createTask(
67
+ wsId: string,
68
+ listId: string,
69
+ task: Partial<Task> & {
70
+ description_yjs_state?: number[];
71
+ label_ids?: string[];
72
+ assignee_ids?: string[];
73
+ project_ids?: string[];
74
+ }
75
+ ) {
76
+ if (!task.name || task.name.trim().length === 0) {
77
+ throw new Error('Task name is required');
78
+ }
79
+
80
+ if (!listId) {
81
+ throw new Error('List ID is required');
82
+ }
83
+
84
+ if (!wsId) {
85
+ throw new Error('Workspace ID is required');
86
+ }
87
+
88
+ const schedulingInput = task as Partial<{
89
+ total_duration: number | null;
90
+ is_splittable: boolean | null;
91
+ min_split_duration_minutes: number | null;
92
+ max_split_duration_minutes: number | null;
93
+ calendar_hours: Task['calendar_hours'];
94
+ auto_schedule: boolean | null;
95
+ }>;
96
+
97
+ const options = await getMutationApiOptions();
98
+ const { task: createdTask } = await createWorkspaceTask(
99
+ wsId,
100
+ {
101
+ name: task.name.trim(),
102
+ description: task.description || null,
103
+ description_yjs_state: task.description_yjs_state ?? null,
104
+ listId,
105
+ priority: task.priority || null,
106
+ start_date: task.start_date || null,
107
+ end_date: task.end_date || null,
108
+ estimation_points: task.estimation_points ?? null,
109
+ label_ids: task.label_ids ?? [],
110
+ assignee_ids: task.assignee_ids ?? [],
111
+ project_ids: task.project_ids ?? [],
112
+ total_duration: schedulingInput.total_duration ?? null,
113
+ is_splittable: schedulingInput.is_splittable ?? null,
114
+ min_split_duration_minutes:
115
+ schedulingInput.min_split_duration_minutes ?? null,
116
+ max_split_duration_minutes:
117
+ schedulingInput.max_split_duration_minutes ?? null,
118
+ calendar_hours: schedulingInput.calendar_hours ?? null,
119
+ auto_schedule: schedulingInput.auto_schedule ?? null,
120
+ },
121
+ options
122
+ );
123
+
124
+ return createdTask as Task;
125
+ }
126
+
127
+ export async function updateTask(
128
+ wsId: string,
129
+ taskId: string,
130
+ task: Partial<Task> & { completed?: boolean }
131
+ ) {
132
+ if (!wsId) {
133
+ throw new Error('Workspace ID is required');
134
+ }
135
+
136
+ const options = await getMutationApiOptions();
137
+ const { task: data } = await updateWorkspaceTask(
138
+ wsId,
139
+ taskId,
140
+ toWorkspaceTaskUpdatePayload(task),
141
+ options
142
+ );
143
+
144
+ if ((task.name !== undefined || task.description !== undefined) && data) {
145
+ triggerWorkspaceTaskEmbedding(wsId, taskId, options).catch(
146
+ (err: unknown) => {
147
+ console.error('Failed to regenerate embedding:', err);
148
+ }
149
+ );
150
+ }
151
+
152
+ return data as Task;
153
+ }
154
+
155
+ export async function invalidateTaskCaches(
156
+ queryClient: QueryClient,
157
+ boardId?: string
158
+ ) {
159
+ const promises: Promise<void>[] = [];
160
+
161
+ if (boardId) {
162
+ promises.push(
163
+ queryClient.invalidateQueries({ queryKey: ['tasks', boardId] })
164
+ );
165
+ promises.push(
166
+ queryClient.invalidateQueries({ queryKey: ['task_lists', boardId] })
167
+ );
168
+ }
169
+
170
+ promises.push(
171
+ queryClient.invalidateQueries({ queryKey: ['time-tracking-data'] })
172
+ );
173
+
174
+ await Promise.all(promises);
175
+ }
176
+
177
+ export async function syncTaskArchivedStatus(
178
+ wsId: string,
179
+ taskId: string,
180
+ listId: string,
181
+ options?: InternalApiClientOptions
182
+ ) {
183
+ const clientOptions = options ?? getBrowserApiOptions();
184
+ const [{ boards }, { task }] = await Promise.all([
185
+ listWorkspaceBoardsWithLists(wsId, clientOptions),
186
+ getWorkspaceTask(wsId, taskId, clientOptions),
187
+ ]);
188
+
189
+ const list = boards
190
+ .flatMap((board) => board.task_lists)
191
+ .find((entry) => entry.id === listId);
192
+
193
+ if (!list) {
194
+ console.error('Error fetching list status: list not found', { listId });
195
+ throw new Error(`Task list not found: ${listId}`);
196
+ }
197
+ const updates = buildTaskArchivedStatusUpdates(task, list.status);
198
+
199
+ if (Object.keys(updates).length === 0) {
200
+ return;
201
+ }
202
+
203
+ try {
204
+ const mutationOptions = options ?? (await getMutationApiOptions());
205
+ await updateWorkspaceTask(wsId, taskId, updates, mutationOptions);
206
+ } catch (updateError) {
207
+ console.error('Error syncing task archived status:', updateError);
208
+ throw updateError;
209
+ }
210
+ }
211
+
212
+ export async function moveTask(
213
+ wsId: string,
214
+ taskId: string,
215
+ newListId: string,
216
+ options?: InternalApiClientOptions
217
+ ) {
218
+ const clientOptions = options ?? getBrowserApiOptions();
219
+ const [{ boards }, { task }] = await Promise.all([
220
+ listWorkspaceBoardsWithLists(wsId, clientOptions),
221
+ getWorkspaceTask(wsId, taskId, clientOptions),
222
+ ]);
223
+
224
+ const targetList = boards
225
+ .flatMap((board) => board.task_lists)
226
+ .find((list) => list.id === newListId);
227
+
228
+ if (!targetList) {
229
+ throw new Error(`Task list not found: ${newListId}`);
230
+ }
231
+
232
+ const updates = buildTaskArchivedStatusUpdates(task, targetList.status);
233
+ const mutationOptions = options ?? (await getMutationApiOptions());
234
+
235
+ await updateWorkspaceTask(
236
+ wsId,
237
+ taskId,
238
+ {
239
+ list_id: newListId,
240
+ ...updates,
241
+ },
242
+ mutationOptions
243
+ );
244
+
245
+ const { task: updatedTask } = await getWorkspaceTask(
246
+ wsId,
247
+ taskId,
248
+ clientOptions
249
+ );
250
+ return updatedTask as Task;
251
+ }
252
+
253
+ export async function moveTaskToBoard(
254
+ wsId: string,
255
+ taskId: string,
256
+ newListId: string,
257
+ targetBoardId?: string
258
+ ) {
259
+ if (!wsId) {
260
+ throw new Error('Workspace ID is required');
261
+ }
262
+
263
+ const options = await getMutationApiOptions();
264
+ const result = await moveWorkspaceTask(
265
+ wsId,
266
+ taskId,
267
+ {
268
+ list_id: newListId,
269
+ target_board_id: targetBoardId,
270
+ },
271
+ options
272
+ );
273
+
274
+ return {
275
+ ...result,
276
+ task: result.task as Task,
277
+ };
278
+ }
@@ -0,0 +1,12 @@
1
+ export { reorderTask, useReorderTask } from './task/reorder';
2
+ export { transformAssignees, transformTaskRecord } from './task/transformers';
3
+ export * from './task-helper/board';
4
+ export * from './task-helper/bulk-actions';
5
+ export * from './task-helper/personal-external-staging';
6
+ export * from './task-helper/recycle-bin';
7
+ export * from './task-helper/relationships';
8
+ export { getTicketIdentifier } from './task-helper/shared';
9
+ export * from './task-helper/sort-keys';
10
+ export * from './task-helper/task-hooks-basic';
11
+ export * from './task-helper/task-hooks-move';
12
+ export * from './task-helper/task-operations';