@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,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
|
+
}
|