@tuturuuu/utils 0.0.2 → 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 +122 -3
  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,202 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import {
3
+ deleteWorkspaceTask,
4
+ listWorkspaceTasks,
5
+ updateWorkspaceTask,
6
+ } from '@tuturuuu/internal-api/tasks';
7
+ import type { Task } from '@tuturuuu/types/primitives/Task';
8
+
9
+ import { transformTaskRecord } from '../task/transformers';
10
+ import { getBrowserApiOptions } from './shared';
11
+
12
+ type UseDeletedTasksOptions = {
13
+ enabled?: boolean;
14
+ staleTime?: number;
15
+ };
16
+
17
+ export function useDeletedTasks(
18
+ boardId: string,
19
+ wsId: string,
20
+ options?: UseDeletedTasksOptions
21
+ ) {
22
+ return useQuery({
23
+ queryKey: ['deleted-tasks', boardId],
24
+ queryFn: async () => {
25
+ const clientOptions = getBrowserApiOptions();
26
+ const { tasks } = await listWorkspaceTasks(
27
+ wsId,
28
+ {
29
+ boardId,
30
+ includeDeleted: 'only',
31
+ },
32
+ clientOptions
33
+ );
34
+
35
+ return tasks.map((task) => transformTaskRecord(task));
36
+ },
37
+ enabled: options?.enabled,
38
+ staleTime: options?.staleTime ?? 5 * 60 * 1000,
39
+ });
40
+ }
41
+
42
+ export function useRestoreTasks(boardId: string, wsId: string) {
43
+ const queryClient = useQueryClient();
44
+
45
+ return useMutation({
46
+ mutationFn: async ({
47
+ taskIds,
48
+ fallbackListId,
49
+ }: {
50
+ taskIds: string[];
51
+ fallbackListId?: string;
52
+ }) => {
53
+ const restoredTasks: Task[] = [];
54
+ for (const taskId of taskIds) {
55
+ try {
56
+ const { task } = await updateWorkspaceTask(
57
+ wsId,
58
+ taskId,
59
+ { deleted: false },
60
+ getBrowserApiOptions()
61
+ );
62
+ restoredTasks.push(task as Task);
63
+ } catch (error) {
64
+ if (!fallbackListId) {
65
+ throw error;
66
+ }
67
+
68
+ const { task } = await updateWorkspaceTask(
69
+ wsId,
70
+ taskId,
71
+ { deleted: false, list_id: fallbackListId },
72
+ getBrowserApiOptions()
73
+ );
74
+ restoredTasks.push(task as Task);
75
+ }
76
+ }
77
+
78
+ return restoredTasks;
79
+ },
80
+ onMutate: async ({ taskIds }) => {
81
+ await queryClient.cancelQueries({ queryKey: ['deleted-tasks', boardId] });
82
+ await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
83
+
84
+ const previousDeletedTasks = queryClient.getQueryData([
85
+ 'deleted-tasks',
86
+ boardId,
87
+ ]) as Task[] | undefined;
88
+ const previousTasks = queryClient.getQueryData(['tasks', boardId]) as
89
+ | Task[]
90
+ | undefined;
91
+
92
+ const restoringTasks =
93
+ previousDeletedTasks?.filter((t) => taskIds.includes(t.id)) || [];
94
+
95
+ queryClient.setQueryData(
96
+ ['deleted-tasks', boardId],
97
+ (old: Task[] | undefined) => {
98
+ if (!old) return old;
99
+ return old.filter((task) => !taskIds.includes(task.id));
100
+ }
101
+ );
102
+
103
+ queryClient.setQueryData(
104
+ ['tasks', boardId],
105
+ (old: Task[] | undefined) => {
106
+ if (!old)
107
+ return restoringTasks.map((t) => ({ ...t, deleted_at: null }));
108
+ return [
109
+ ...old,
110
+ ...restoringTasks.map((t) => ({ ...t, deleted_at: null })),
111
+ ];
112
+ }
113
+ );
114
+
115
+ return { previousDeletedTasks, previousTasks };
116
+ },
117
+ onError: (err, _, context) => {
118
+ if (context?.previousDeletedTasks) {
119
+ queryClient.setQueryData(
120
+ ['deleted-tasks', boardId],
121
+ context.previousDeletedTasks
122
+ );
123
+ }
124
+ if (context?.previousTasks) {
125
+ queryClient.setQueryData(['tasks', boardId], context.previousTasks);
126
+ }
127
+
128
+ queryClient.invalidateQueries({ queryKey: ['deleted-tasks', boardId] });
129
+ queryClient.invalidateQueries({ queryKey: ['tasks', boardId] });
130
+ console.error('Failed to restore tasks:', err);
131
+ },
132
+ onSuccess: (restoredTasks) => {
133
+ queryClient.setQueryData(
134
+ ['tasks', boardId],
135
+ (old: Task[] | undefined) => {
136
+ if (!old) return restoredTasks;
137
+ const restoredIds = new Set(restoredTasks.map((t) => t.id));
138
+ const otherTasks = old.filter((t) => !restoredIds.has(t.id));
139
+ return [...otherTasks, ...restoredTasks];
140
+ }
141
+ );
142
+ },
143
+ });
144
+ }
145
+
146
+ export function usePermanentlyDeleteTasks(boardId: string, wsId: string) {
147
+ const queryClient = useQueryClient();
148
+
149
+ return useMutation({
150
+ mutationFn: async (taskIds: string[]) => {
151
+ let count = 0;
152
+ const failedTaskIds: string[] = [];
153
+
154
+ for (const taskId of taskIds) {
155
+ try {
156
+ await deleteWorkspaceTask(wsId, taskId, getBrowserApiOptions());
157
+ count++;
158
+ } catch (error) {
159
+ console.error(`Failed to permanently delete task ${taskId}:`, error);
160
+ failedTaskIds.push(taskId);
161
+ }
162
+ }
163
+
164
+ if (failedTaskIds.length > 0) {
165
+ throw new Error(
166
+ `Failed to permanently delete ${failedTaskIds.length} task${failedTaskIds.length === 1 ? '' : 's'}`
167
+ );
168
+ }
169
+
170
+ return { count, failedTaskIds };
171
+ },
172
+ onMutate: async (taskIds) => {
173
+ await queryClient.cancelQueries({ queryKey: ['deleted-tasks', boardId] });
174
+
175
+ const previousDeletedTasks = queryClient.getQueryData([
176
+ 'deleted-tasks',
177
+ boardId,
178
+ ]) as Task[] | undefined;
179
+
180
+ queryClient.setQueryData(
181
+ ['deleted-tasks', boardId],
182
+ (old: Task[] | undefined) => {
183
+ if (!old) return old;
184
+ return old.filter((task) => !taskIds.includes(task.id));
185
+ }
186
+ );
187
+
188
+ return { previousDeletedTasks };
189
+ },
190
+ onError: (err, _, context) => {
191
+ if (context?.previousDeletedTasks) {
192
+ queryClient.setQueryData(
193
+ ['deleted-tasks', boardId],
194
+ context.previousDeletedTasks
195
+ );
196
+ }
197
+
198
+ queryClient.invalidateQueries({ queryKey: ['deleted-tasks', boardId] });
199
+ console.error('Failed to permanently delete tasks:', err);
200
+ },
201
+ });
202
+ }
@@ -0,0 +1,346 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import {
3
+ createWorkspaceTaskRelationship,
4
+ createWorkspaceTaskWithRelationship,
5
+ deleteWorkspaceTaskRelationship,
6
+ getWorkspaceTaskRelationships,
7
+ listWorkspaceTasks,
8
+ } from '@tuturuuu/internal-api/tasks';
9
+ import { isTaskPriority } from '@tuturuuu/types/primitives/Priority';
10
+ import type { Task } from '@tuturuuu/types/primitives/Task';
11
+ import type {
12
+ CreateTaskRelationshipInput,
13
+ CreateTaskWithRelationshipInput,
14
+ RelatedTaskInfo,
15
+ TaskRelationship,
16
+ TaskRelationshipsResponse,
17
+ TaskRelationshipType,
18
+ } from '@tuturuuu/types/primitives/TaskRelationship';
19
+
20
+ import {
21
+ getBrowserApiOptions,
22
+ getMutationApiOptions,
23
+ getTaskIdentifierForSearch,
24
+ isTicketIdentifierLikeQuery,
25
+ normalizeTaskSearchValue,
26
+ } from './shared';
27
+
28
+ export type TaskRelationshipErrorCode =
29
+ | 'already_exists'
30
+ | 'single_parent'
31
+ | 'circular';
32
+
33
+ export class TaskRelationshipError extends Error {
34
+ constructor(public readonly code: TaskRelationshipErrorCode) {
35
+ super(code);
36
+ this.name = 'TaskRelationshipError';
37
+ }
38
+ }
39
+
40
+ export async function getTaskRelationships(
41
+ wsId: string,
42
+ taskId: string
43
+ ): Promise<TaskRelationshipsResponse> {
44
+ const payload = await getWorkspaceTaskRelationships(
45
+ wsId,
46
+ taskId,
47
+ getBrowserApiOptions()
48
+ );
49
+ return payload;
50
+ }
51
+
52
+ export async function createTaskRelationship(
53
+ wsId: string,
54
+ input: CreateTaskRelationshipInput
55
+ ): Promise<TaskRelationship> {
56
+ const options = await getMutationApiOptions();
57
+
58
+ try {
59
+ const { relationship } = await createWorkspaceTaskRelationship(
60
+ wsId,
61
+ input.source_task_id,
62
+ input,
63
+ options
64
+ );
65
+ return relationship;
66
+ } catch (error) {
67
+ const typedError = error as Error & { code?: string };
68
+ if (
69
+ typedError.code === '23505' ||
70
+ typedError.message?.includes('already exists')
71
+ ) {
72
+ throw new TaskRelationshipError('already_exists');
73
+ }
74
+ if (typedError.message?.includes('single parent')) {
75
+ throw new TaskRelationshipError('single_parent');
76
+ }
77
+ if (typedError.message?.includes('circular')) {
78
+ throw new TaskRelationshipError('circular');
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ export async function deleteTaskRelationship(
85
+ wsId: string,
86
+ relationship?: Pick<
87
+ TaskRelationship,
88
+ 'source_task_id' | 'target_task_id' | 'type'
89
+ >
90
+ ): Promise<void> {
91
+ if (!relationship) {
92
+ throw new Error(
93
+ 'deleteTaskRelationship requires source_task_id, target_task_id, and type.'
94
+ );
95
+ }
96
+
97
+ const options = await getMutationApiOptions();
98
+ await deleteWorkspaceTaskRelationship(
99
+ wsId,
100
+ relationship.source_task_id,
101
+ {
102
+ source_task_id: relationship.source_task_id,
103
+ target_task_id: relationship.target_task_id,
104
+ type: relationship.type,
105
+ },
106
+ options
107
+ );
108
+ }
109
+
110
+ export function useTaskRelationships(
111
+ taskId: string | undefined,
112
+ wsId?: string
113
+ ) {
114
+ return useQuery({
115
+ queryKey: ['task-relationships', taskId, wsId],
116
+ queryFn: async () => {
117
+ if (!taskId || !wsId) return null;
118
+ const relationships = await getWorkspaceTaskRelationships(
119
+ wsId,
120
+ taskId,
121
+ getBrowserApiOptions()
122
+ );
123
+ return relationships;
124
+ },
125
+ enabled: !!taskId && !!wsId,
126
+ staleTime: 30000,
127
+ });
128
+ }
129
+
130
+ export function useCreateTaskRelationship(wsId: string) {
131
+ const queryClient = useQueryClient();
132
+
133
+ return useMutation({
134
+ mutationFn: async (input: CreateTaskRelationshipInput) =>
135
+ createTaskRelationship(wsId, input),
136
+ onSuccess: async (_, variables) => {
137
+ await Promise.all([
138
+ queryClient.invalidateQueries({
139
+ queryKey: ['task-relationships', variables.source_task_id],
140
+ }),
141
+ queryClient.invalidateQueries({
142
+ queryKey: ['task-relationships', variables.target_task_id],
143
+ }),
144
+ ]);
145
+ },
146
+ });
147
+ }
148
+
149
+ export function useDeleteTaskRelationship(wsId: string) {
150
+ const queryClient = useQueryClient();
151
+
152
+ return useMutation({
153
+ mutationFn: async ({
154
+ sourceTaskId,
155
+ targetTaskId,
156
+ type,
157
+ }: {
158
+ sourceTaskId: string;
159
+ targetTaskId: string;
160
+ type: TaskRelationshipType;
161
+ }) => {
162
+ return deleteTaskRelationship(wsId, {
163
+ source_task_id: sourceTaskId,
164
+ target_task_id: targetTaskId,
165
+ type,
166
+ });
167
+ },
168
+ onSuccess: async (_, variables) => {
169
+ await Promise.all([
170
+ queryClient.invalidateQueries({
171
+ queryKey: ['task-relationships', variables.sourceTaskId],
172
+ }),
173
+ queryClient.invalidateQueries({
174
+ queryKey: ['task-relationships', variables.targetTaskId],
175
+ }),
176
+ ]);
177
+ },
178
+ });
179
+ }
180
+
181
+ export async function getWorkspaceTasks(
182
+ wsId: string,
183
+ options?: {
184
+ excludeTaskIds?: string[];
185
+ searchQuery?: string;
186
+ limit?: number;
187
+ }
188
+ ): Promise<RelatedTaskInfo[]> {
189
+ const excluded = new Set(options?.excludeTaskIds ?? []);
190
+ const normalizedSearch = options?.searchQuery
191
+ ? normalizeTaskSearchValue(options.searchQuery)
192
+ : '';
193
+ const ticketLikeSearch = normalizedSearch
194
+ ? isTicketIdentifierLikeQuery(normalizedSearch)
195
+ : false;
196
+ const targetLimit = options?.limit ?? 50;
197
+ const requestLimit = Math.max(25, Math.min(targetLimit * 2, 200));
198
+ const collected: RelatedTaskInfo[] = [];
199
+ const seenTaskIds = new Set<string>();
200
+
201
+ const clientOptions =
202
+ typeof window !== 'undefined'
203
+ ? {
204
+ baseUrl: window.location.origin,
205
+ fetch: (input: RequestInfo | URL, init?: RequestInit) =>
206
+ fetch(input, { ...init, cache: init?.cache ?? 'no-store' }),
207
+ }
208
+ : undefined;
209
+
210
+ let offset = 0;
211
+ while (true) {
212
+ const { tasks } = await listWorkspaceTasks(
213
+ wsId,
214
+ {
215
+ q: ticketLikeSearch ? undefined : options?.searchQuery,
216
+ identifier: ticketLikeSearch ? options?.searchQuery : undefined,
217
+ limit: requestLimit,
218
+ offset,
219
+ includeRelationshipSummary: !ticketLikeSearch,
220
+ },
221
+ clientOptions
222
+ );
223
+
224
+ const pageTasks = tasks ?? [];
225
+ if (pageTasks.length === 0) {
226
+ break;
227
+ }
228
+
229
+ for (const task of pageTasks) {
230
+ if (excluded.has(task.id) || seenTaskIds.has(task.id)) {
231
+ continue;
232
+ }
233
+
234
+ if (normalizedSearch) {
235
+ const taskName = normalizeTaskSearchValue(task.name ?? '');
236
+ const taskIdentifier = normalizeTaskSearchValue(
237
+ getTaskIdentifierForSearch(task) ?? ''
238
+ );
239
+
240
+ if (
241
+ !taskName.includes(normalizedSearch) &&
242
+ !taskIdentifier.includes(normalizedSearch)
243
+ ) {
244
+ continue;
245
+ }
246
+ }
247
+
248
+ seenTaskIds.add(task.id);
249
+ collected.push({
250
+ id: task.id,
251
+ name: task.name,
252
+ display_number: task.display_number,
253
+ ticket_prefix: task.ticket_prefix ?? null,
254
+ completed: !!task.closed_at || !!task.completed_at,
255
+ priority: isTaskPriority(task.priority) ? task.priority : null,
256
+ board_id: task.board_id ?? null,
257
+ board_name: task.board_name,
258
+ });
259
+
260
+ if (collected.length >= targetLimit) {
261
+ return collected;
262
+ }
263
+ }
264
+
265
+ if (pageTasks.length < requestLimit) {
266
+ break;
267
+ }
268
+
269
+ offset += requestLimit;
270
+ }
271
+
272
+ return collected;
273
+ }
274
+
275
+ export function useWorkspaceTasks(
276
+ wsId: string | undefined,
277
+ options?: {
278
+ excludeTaskIds?: string[];
279
+ searchQuery?: string;
280
+ limit?: number;
281
+ enabled?: boolean;
282
+ }
283
+ ) {
284
+ return useQuery({
285
+ queryKey: [
286
+ 'workspace-tasks',
287
+ wsId,
288
+ options?.excludeTaskIds,
289
+ options?.searchQuery,
290
+ options?.limit,
291
+ ],
292
+ queryFn: async () => {
293
+ if (!wsId) return [];
294
+ return getWorkspaceTasks(wsId, options);
295
+ },
296
+ enabled: !!wsId && (options?.enabled ?? true),
297
+ staleTime: 30000,
298
+ });
299
+ }
300
+
301
+ export function useCreateTaskWithRelationship(boardId: string, wsId: string) {
302
+ const queryClient = useQueryClient();
303
+
304
+ return useMutation({
305
+ mutationFn: async (input: CreateTaskWithRelationshipInput) => {
306
+ const result = await createWorkspaceTaskWithRelationship(
307
+ wsId,
308
+ input,
309
+ getBrowserApiOptions()
310
+ );
311
+ return {
312
+ task: result.task as Task,
313
+ relationship: result.relationship,
314
+ };
315
+ },
316
+ onSuccess: async (result, variables) => {
317
+ const locallyCreatedTask = {
318
+ ...(result.task as Task & { _localMutationAt?: number }),
319
+ _localMutationAt: Date.now(),
320
+ } as Task;
321
+
322
+ if (boardId) {
323
+ queryClient.setQueryData(
324
+ ['tasks', boardId],
325
+ (old: Task[] | undefined) => {
326
+ if (!old) return [locallyCreatedTask];
327
+ if (old.some((t) => t.id === result.task.id)) return old;
328
+ return [...old, locallyCreatedTask];
329
+ }
330
+ );
331
+ }
332
+
333
+ await Promise.all([
334
+ queryClient.invalidateQueries({
335
+ queryKey: ['task-relationships', variables.currentTaskId],
336
+ }),
337
+ queryClient.invalidateQueries({
338
+ queryKey: ['task-relationships', result.task.id],
339
+ }),
340
+ queryClient.invalidateQueries({
341
+ queryKey: ['workspace-tasks', wsId],
342
+ }),
343
+ ]);
344
+ },
345
+ });
346
+ }
@@ -0,0 +1,109 @@
1
+ import type { InternalApiClientOptions } from '@tuturuuu/internal-api/client';
2
+ import { listWorkspaceTasks } from '@tuturuuu/internal-api/tasks';
3
+ import type { Task } from '@tuturuuu/types/primitives/Task';
4
+
5
+ export function getTicketIdentifier(
6
+ prefix: string | null | undefined,
7
+ displayNumber: number
8
+ ): string {
9
+ const effectivePrefix = prefix?.trim() || 'TASK';
10
+ return `${effectivePrefix}-${displayNumber}`.toUpperCase();
11
+ }
12
+
13
+ export function normalizeTaskSearchValue(value: string) {
14
+ return value.trim().toLowerCase();
15
+ }
16
+
17
+ export function isTicketIdentifierLikeQuery(query: string) {
18
+ const normalized = normalizeTaskSearchValue(query);
19
+ if (!normalized) return false;
20
+ return /^\d+$/.test(normalized) || /^[a-z][a-z0-9_-]*-\d+$/.test(normalized);
21
+ }
22
+
23
+ export function getTaskIdentifierForSearch(task: {
24
+ ticket_prefix?: string | null;
25
+ display_number?: number | null;
26
+ }) {
27
+ if (typeof task.display_number !== 'number') {
28
+ return null;
29
+ }
30
+
31
+ return getTicketIdentifier(task.ticket_prefix, task.display_number);
32
+ }
33
+
34
+ export async function getMutationApiOptions(): Promise<
35
+ InternalApiClientOptions | undefined
36
+ > {
37
+ if (typeof window !== 'undefined') {
38
+ return { baseUrl: window.location.origin };
39
+ }
40
+
41
+ return undefined;
42
+ }
43
+
44
+ export function getBrowserApiOptions(): InternalApiClientOptions | undefined {
45
+ if (typeof window === 'undefined') {
46
+ return undefined;
47
+ }
48
+
49
+ return { baseUrl: window.location.origin };
50
+ }
51
+
52
+ export async function listAllActiveTasksForList(wsId: string, listId: string) {
53
+ const tasks: Task[] = [];
54
+ const pageSize = 200;
55
+ let offset = 0;
56
+
57
+ while (true) {
58
+ const { tasks: page } = await listWorkspaceTasks(
59
+ wsId,
60
+ {
61
+ listId,
62
+ limit: pageSize,
63
+ offset,
64
+ includeDeleted: false,
65
+ },
66
+ getBrowserApiOptions()
67
+ );
68
+
69
+ if (!page.length) {
70
+ break;
71
+ }
72
+
73
+ tasks.push(...(page as Task[]));
74
+
75
+ if (page.length < pageSize) {
76
+ break;
77
+ }
78
+
79
+ offset += pageSize;
80
+ }
81
+
82
+ return tasks;
83
+ }
84
+
85
+ export function toWorkspaceTaskUpdatePayload(
86
+ task: Partial<Task> & { completed?: boolean }
87
+ ) {
88
+ return {
89
+ ...(task.name !== undefined ? { name: task.name } : {}),
90
+ ...(task.description !== undefined
91
+ ? { description: task.description }
92
+ : {}),
93
+ ...(task.priority !== undefined ? { priority: task.priority } : {}),
94
+ ...(task.start_date !== undefined ? { start_date: task.start_date } : {}),
95
+ ...(task.end_date !== undefined ? { end_date: task.end_date } : {}),
96
+ ...(task.closed_at !== undefined ? { closed_at: task.closed_at } : {}),
97
+ ...(task.completed_at !== undefined
98
+ ? { completed_at: task.completed_at }
99
+ : {}),
100
+ ...(task.list_id !== undefined ? { list_id: task.list_id } : {}),
101
+ ...(task.estimation_points !== undefined
102
+ ? { estimation_points: task.estimation_points }
103
+ : {}),
104
+ ...(task.completed !== undefined ? { completed: task.completed } : {}),
105
+ ...(task.sort_key !== undefined && task.sort_key !== null
106
+ ? { sort_key: task.sort_key }
107
+ : {}),
108
+ };
109
+ }