@tuturuuu/ui 0.11.1 → 0.13.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 (49) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +5 -5
  3. package/src/components/ui/calendar-app/components/habits-panel.tsx +5 -2
  4. package/src/components/ui/calendar-app/components/priority-view.tsx +15 -7
  5. package/src/components/ui/calendar-app/components/scheduling-dialog.tsx +7 -3
  6. package/src/components/ui/calendar-app/components/task-scheduler-panel.tsx +7 -3
  7. package/src/components/ui/calendar-app/components/time-tracker/index.tsx +14 -9
  8. package/src/components/ui/custom/settings/task-settings.tsx +6 -2
  9. package/src/components/ui/custom/tables/data-table.tsx +58 -8
  10. package/src/components/ui/storefront/bundle-selection-dialog.tsx +249 -0
  11. package/src/components/ui/storefront/cart-summary-parts.tsx +28 -8
  12. package/src/components/ui/storefront/listing-card.tsx +6 -3
  13. package/src/components/ui/storefront/storefront-surface.test.tsx +134 -1
  14. package/src/components/ui/storefront/storefront-surface.tsx +66 -7
  15. package/src/components/ui/storefront/types.ts +15 -0
  16. package/src/components/ui/storefront/utils.ts +164 -2
  17. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -1
  18. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +3 -6
  19. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +11 -1
  20. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -3
  21. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-resource-context.test.ts +113 -0
  22. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-resource-context.ts +50 -0
  23. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +65 -18
  24. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +1 -1
  25. package/src/components/ui/tu-do/cycles/task-cycles-client.tsx +23 -14
  26. package/src/components/ui/tu-do/estimates/use-task-estimates.ts +9 -4
  27. package/src/components/ui/tu-do/habits/client.tsx +4 -1
  28. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +6 -3
  29. package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +19 -11
  30. package/src/components/ui/tu-do/my-tasks/use-my-tasks-query.ts +15 -6
  31. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +18 -13
  32. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +17 -10
  33. package/src/components/ui/tu-do/shared/fade-setting-initializer.tsx +3 -1
  34. package/src/components/ui/tu-do/shared/list-view.tsx +1 -1
  35. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +6 -2
  36. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-update-shared-task.test.ts +9 -5
  37. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +13 -8
  38. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-overrides.ts +21 -9
  39. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-update-shared-task.ts +10 -5
  40. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +4 -2
  41. package/src/components/ui/tu-do/shared/task-estimation-picker.tsx +5 -2
  42. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +1 -1
  43. package/src/hooks/__tests__/use-task-actions.test.tsx +51 -0
  44. package/src/hooks/task-actions-personal-external.ts +2 -3
  45. package/src/hooks/use-board-actions.ts +27 -17
  46. package/src/hooks/use-calendar.tsx +7 -3
  47. package/src/hooks/use-settings-dialog-shortcut.ts +51 -0
  48. package/src/lib/task-personal-external.ts +49 -0
  49. package/src/lib/tasks-app-url.ts +85 -0
@@ -128,9 +128,10 @@ describe('useTaskContextActions', () => {
128
128
  });
129
129
 
130
130
  expect(global.fetch).toHaveBeenCalledWith(
131
- `/api/v1/users/me/tasks/${mockTask.id}/overrides`,
131
+ `http://localhost:7809/api/v1/users/me/tasks/${mockTask.id}/overrides`,
132
132
  {
133
133
  method: 'PUT',
134
+ credentials: 'include',
134
135
  headers: { 'Content-Type': 'application/json' },
135
136
  body: JSON.stringify({ personally_unassigned: true }),
136
137
  }
@@ -160,9 +161,10 @@ describe('useTaskContextActions', () => {
160
161
  });
161
162
 
162
163
  expect(global.fetch).toHaveBeenCalledWith(
163
- `/api/v1/users/me/tasks/${mockTask.id}/overrides`,
164
+ `http://localhost:7809/api/v1/users/me/tasks/${mockTask.id}/overrides`,
164
165
  {
165
166
  method: 'PUT',
167
+ credentials: 'include',
166
168
  headers: { 'Content-Type': 'application/json' },
167
169
  body: JSON.stringify({
168
170
  personally_unassigned: false,
@@ -219,9 +221,10 @@ describe('useTaskContextActions', () => {
219
221
 
220
222
  // Verify override cleanup
221
223
  expect(global.fetch).toHaveBeenCalledWith(
222
- `/api/v1/users/me/tasks/${taskWithOverrides.id}/overrides`,
224
+ `http://localhost:7809/api/v1/users/me/tasks/${taskWithOverrides.id}/overrides`,
223
225
  expect.objectContaining({
224
226
  method: 'PUT',
227
+ credentials: 'include',
225
228
  headers: { 'Content-Type': 'application/json' },
226
229
  })
227
230
  );
@@ -34,6 +34,7 @@ import {
34
34
  import Link from 'next/link';
35
35
  import { useTranslations } from 'next-intl';
36
36
  import { type MouseEvent, useState } from 'react';
37
+ import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
37
38
  import { useTasksHref } from '../tasks-route-context';
38
39
  import { MyTaskContextMenu } from './my-task-context-menu';
39
40
  import { PersonalPlacementDialog } from './personal-placement-dialog';
@@ -128,9 +129,10 @@ export default function TaskListWithCompletion({
128
129
  setCompletingTasks((prev) => new Set(prev).add(task.id));
129
130
  try {
130
131
  const response = await fetch(
131
- `/api/v1/users/me/tasks/${task.id}/overrides`,
132
+ getTaskApiUrl(`/api/v1/users/me/tasks/${task.id}/overrides`),
132
133
  {
133
134
  method: 'PUT',
135
+ credentials: 'include',
134
136
  headers: { 'Content-Type': 'application/json' },
135
137
  body: JSON.stringify({ personally_unassigned: false }),
136
138
  }
@@ -160,9 +162,10 @@ export default function TaskListWithCompletion({
160
162
 
161
163
  try {
162
164
  const response = await fetch(
163
- `/api/v1/users/me/tasks/${task.id}/overrides`,
165
+ getTaskApiUrl(`/api/v1/users/me/tasks/${task.id}/overrides`),
164
166
  {
165
167
  method: 'PUT',
168
+ credentials: 'include',
166
169
  headers: { 'Content-Type': 'application/json' },
167
170
  body: JSON.stringify({
168
171
  completed_at: isAlreadyDone ? null : new Date().toISOString(),
@@ -234,14 +237,18 @@ export default function TaskListWithCompletion({
234
237
  !isCompleted &&
235
238
  (task.overrides?.completed_at || task.overrides?.personally_unassigned)
236
239
  ) {
237
- await fetch(`/api/v1/users/me/tasks/${task.id}/overrides`, {
238
- method: 'PUT',
239
- headers: { 'Content-Type': 'application/json' },
240
- body: JSON.stringify({
241
- completed_at: null,
242
- personally_unassigned: false,
243
- }),
244
- }).catch(() => {
240
+ await fetch(
241
+ getTaskApiUrl(`/api/v1/users/me/tasks/${task.id}/overrides`),
242
+ {
243
+ method: 'PUT',
244
+ credentials: 'include',
245
+ headers: { 'Content-Type': 'application/json' },
246
+ body: JSON.stringify({
247
+ completed_at: null,
248
+ personally_unassigned: false,
249
+ }),
250
+ }
251
+ ).catch(() => {
245
252
  // Non-critical cleanup — don't block the completion flow
246
253
  });
247
254
  }
@@ -275,9 +282,10 @@ export default function TaskListWithCompletion({
275
282
 
276
283
  try {
277
284
  const response = await fetch(
278
- `/api/v1/users/me/tasks/${task.id}/overrides`,
285
+ getTaskApiUrl(`/api/v1/users/me/tasks/${task.id}/overrides`),
279
286
  {
280
287
  method: 'PUT',
288
+ credentials: 'include',
281
289
  headers: { 'Content-Type': 'application/json' },
282
290
  body: JSON.stringify({ personally_unassigned: true }),
283
291
  }
@@ -6,6 +6,7 @@ import {
6
6
  useQuery,
7
7
  } from '@tanstack/react-query';
8
8
  import type { TaskWithRelations } from '@tuturuuu/types';
9
+ import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
9
10
 
10
11
  export interface MyTasksData {
11
12
  overdue: TaskWithRelations[];
@@ -62,9 +63,13 @@ export function useMyTasksQuery(
62
63
  }
63
64
  }
64
65
 
65
- const res = await fetch(`/api/v1/users/me/tasks?${params}`, {
66
- cache: 'no-store',
67
- });
66
+ const res = await fetch(
67
+ getTaskApiUrl(`/api/v1/users/me/tasks?${params}`),
68
+ {
69
+ cache: 'no-store',
70
+ credentials: 'include',
71
+ }
72
+ );
68
73
  if (!res.ok) {
69
74
  throw new Error('Failed to fetch tasks');
70
75
  }
@@ -117,9 +122,13 @@ export function useCompletedTasksQuery(
117
122
  }
118
123
  }
119
124
 
120
- const res = await fetch(`/api/v1/users/me/tasks?${params}`, {
121
- cache: 'no-store',
122
- });
125
+ const res = await fetch(
126
+ getTaskApiUrl(`/api/v1/users/me/tasks?${params}`),
127
+ {
128
+ cache: 'no-store',
129
+ credentials: 'include',
130
+ }
131
+ );
123
132
  if (!res.ok) {
124
133
  throw new Error('Failed to fetch completed tasks');
125
134
  }
@@ -25,6 +25,7 @@ import timezone from 'dayjs/plugin/timezone';
25
25
  import utc from 'dayjs/plugin/utc';
26
26
  import { useTranslations } from 'next-intl';
27
27
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
28
+ import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
28
29
  import { getRandomNewLabelColor } from '../utils/taskConstants';
29
30
  import type { TaskOptions } from './command-bar';
30
31
  import type { JournalTaskResponse } from './task-preview-dialog';
@@ -552,19 +553,23 @@ export function useMyTasksState({
552
553
  }
553
554
  >({
554
555
  mutationFn: async (variables) => {
555
- const response = await fetch(`/api/v1/workspaces/${wsId}/tasks/journal`, {
556
- method: 'POST',
557
- headers: { 'Content-Type': 'application/json' },
558
- body: JSON.stringify({
559
- entry: variables.entry,
560
- previewOnly: true,
561
- generateDescriptions: variables.generateDescriptions,
562
- generatePriority: variables.generatePriority,
563
- generateLabels: variables.generateLabels,
564
- clientTimezone: variables.clientTimezone,
565
- clientTimestamp: variables.clientTimestamp,
566
- }),
567
- });
556
+ const response = await fetch(
557
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/tasks/journal`),
558
+ {
559
+ method: 'POST',
560
+ credentials: 'include',
561
+ headers: { 'Content-Type': 'application/json' },
562
+ body: JSON.stringify({
563
+ entry: variables.entry,
564
+ previewOnly: true,
565
+ generateDescriptions: variables.generateDescriptions,
566
+ generatePriority: variables.generatePriority,
567
+ generateLabels: variables.generateLabels,
568
+ clientTimezone: variables.clientTimezone,
569
+ clientTimestamp: variables.clientTimestamp,
570
+ }),
571
+ }
572
+ );
568
573
 
569
574
  const payload = await response.json().catch(() => null);
570
575
  if (!response.ok) {
@@ -14,6 +14,7 @@ import { getActiveTaskUserBroadcast } from '@tuturuuu/ui/hooks/useTaskUserRealti
14
14
  import { toast } from '@tuturuuu/ui/sonner';
15
15
  import { useTranslations } from 'next-intl';
16
16
  import { useCallback, useState } from 'react';
17
+ import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
17
18
  import { dispatchTaskSoundCue } from '../shared/task-sound-effects';
18
19
  import {
19
20
  MY_COMPLETED_TASKS_QUERY_KEY,
@@ -243,14 +244,18 @@ export function useTaskContextActions({
243
244
  task.overrides?.completed_at ||
244
245
  task.overrides?.personally_unassigned
245
246
  ) {
246
- await fetch(`/api/v1/users/me/tasks/${task.id}/overrides`, {
247
- method: 'PUT',
248
- headers: { 'Content-Type': 'application/json' },
249
- body: JSON.stringify({
250
- completed_at: null,
251
- personally_unassigned: false,
252
- }),
253
- }).catch(() => {
247
+ await fetch(
248
+ getTaskApiUrl(`/api/v1/users/me/tasks/${task.id}/overrides`),
249
+ {
250
+ method: 'PUT',
251
+ credentials: 'include',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify({
254
+ completed_at: null,
255
+ personally_unassigned: false,
256
+ }),
257
+ }
258
+ ).catch(() => {
254
259
  // Non-critical cleanup
255
260
  });
256
261
  }
@@ -278,9 +283,10 @@ export function useTaskContextActions({
278
283
  setIsLoading(true);
279
284
  try {
280
285
  const response = await fetch(
281
- `/api/v1/users/me/tasks/${task.id}/overrides`,
286
+ getTaskApiUrl(`/api/v1/users/me/tasks/${task.id}/overrides`),
282
287
  {
283
288
  method: 'PUT',
289
+ credentials: 'include',
284
290
  headers: { 'Content-Type': 'application/json' },
285
291
  body: JSON.stringify({ personally_unassigned: true }),
286
292
  }
@@ -301,9 +307,10 @@ export function useTaskContextActions({
301
307
  setIsLoading(true);
302
308
  try {
303
309
  const response = await fetch(
304
- `/api/v1/users/me/tasks/${task.id}/overrides`,
310
+ getTaskApiUrl(`/api/v1/users/me/tasks/${task.id}/overrides`),
305
311
  {
306
312
  method: 'PUT',
313
+ credentials: 'include',
307
314
  headers: { 'Content-Type': 'application/json' },
308
315
  body: JSON.stringify({
309
316
  personally_unassigned: false,
@@ -3,6 +3,7 @@
3
3
  import { useQuery } from '@tanstack/react-query';
4
4
  import { createClient } from '@tuturuuu/supabase/next/client';
5
5
  import { useEffect, useState } from 'react';
6
+ import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
6
7
 
7
8
  interface TaskSettingsData {
8
9
  task_auto_assign_to_self: boolean;
@@ -32,8 +33,9 @@ export function FadeSettingInitializer() {
32
33
  const { data: settings } = useQuery({
33
34
  queryKey: ['user-task-settings'],
34
35
  queryFn: async (): Promise<TaskSettingsData> => {
35
- const res = await fetch('/api/v1/users/task-settings', {
36
+ const res = await fetch(getTaskApiUrl('/api/v1/users/task-settings'), {
36
37
  cache: 'no-store',
38
+ credentials: 'include',
37
39
  });
38
40
  if (!res.ok) {
39
41
  // Return defaults if API fails
@@ -528,7 +528,7 @@ function InteractiveListView({
528
528
  isPersonalWorkspace,
529
529
  canUseBoardAssignees: task.source_workspace_id ? true : showAssignees,
530
530
  assigneeMemberSource: task.source_workspace_id
531
- ? 'workspace'
531
+ ? 'board'
532
532
  : effectiveAssigneeMemberSource,
533
533
  })
534
534
  );
@@ -9,6 +9,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
9
9
  import { Switch } from '@tuturuuu/ui/switch';
10
10
  import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
11
11
  import { useTranslations } from 'next-intl';
12
+ import { getTaskApiUrl } from '../../../../../../lib/tasks-app-url';
12
13
  import { TASK_SOUND_EFFECTS_ENABLED_CONFIG_ID } from '../../task-sound-effects';
13
14
 
14
15
  interface TaskSettingsData {
@@ -54,7 +55,9 @@ export function QuickSettingsPopover({
54
55
  const { data: settings, isLoading } = useQuery({
55
56
  queryKey: ['user-task-settings'],
56
57
  queryFn: async (): Promise<TaskSettingsData> => {
57
- const res = await fetch('/api/v1/users/task-settings');
58
+ const res = await fetch(getTaskApiUrl('/api/v1/users/task-settings'), {
59
+ credentials: 'include',
60
+ });
58
61
  if (!res.ok) {
59
62
  // Return defaults if API fails
60
63
  return { task_auto_assign_to_self: false, fade_completed_tasks: false };
@@ -68,8 +71,9 @@ export function QuickSettingsPopover({
68
71
 
69
72
  const updateSettings = useMutation({
70
73
  mutationFn: async (data: Partial<TaskSettingsData>) => {
71
- const res = await fetch('/api/v1/users/task-settings', {
74
+ const res = await fetch(getTaskApiUrl('/api/v1/users/task-settings'), {
72
75
  method: 'PATCH',
76
+ credentials: 'include',
73
77
  headers: { 'Content-Type': 'application/json' },
74
78
  body: JSON.stringify(data),
75
79
  });
@@ -48,11 +48,15 @@ describe('useUpdateSharedTask', () => {
48
48
  // Cast to any to access mutationFn which is available in the mock return value
49
49
  const response = await (result.current as any).mutationFn(payload);
50
50
 
51
- expect(global.fetch).toHaveBeenCalledWith('/api/v1/shared/tasks/SHARE123', {
52
- method: 'PATCH',
53
- headers: { 'Content-Type': 'application/json' },
54
- body: JSON.stringify(payload.updates),
55
- });
51
+ expect(global.fetch).toHaveBeenCalledWith(
52
+ 'http://localhost:7809/api/v1/shared/tasks/SHARE123',
53
+ {
54
+ method: 'PATCH',
55
+ credentials: 'include',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify(payload.updates),
58
+ }
59
+ );
56
60
  expect(response).toEqual(mockResponse);
57
61
  });
58
62
 
@@ -14,6 +14,7 @@ import {
14
14
  type WorkspaceTaskDescriptionUpdatePayload,
15
15
  type WorkspaceTaskUpdatePayload,
16
16
  } from '@tuturuuu/internal-api/tasks';
17
+ import { getTaskApiUrl } from '../../../../../../lib/tasks-app-url';
17
18
  import type { WorkspaceTaskLabel } from '../types';
18
19
 
19
20
  const TASK_DESCRIPTION_DIRECT_BODY_LIMIT_BYTES = 192 * 1024;
@@ -261,14 +262,18 @@ export async function createWorkspaceLabel(
261
262
  wsId: string,
262
263
  payload: { name: string; color: string }
263
264
  ) {
264
- const response = await fetch(`/api/v1/workspaces/${wsId}/labels`, {
265
- method: 'POST',
266
- headers: {
267
- 'Content-Type': 'application/json',
268
- },
269
- body: JSON.stringify(payload),
270
- cache: 'no-store',
271
- });
265
+ const response = await fetch(
266
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/labels`),
267
+ {
268
+ method: 'POST',
269
+ credentials: 'include',
270
+ headers: {
271
+ 'Content-Type': 'application/json',
272
+ },
273
+ body: JSON.stringify(payload),
274
+ cache: 'no-store',
275
+ }
276
+ );
272
277
 
273
278
  if (!response.ok) {
274
279
  throw new Error(await getErrorMessage(response, 'Failed to create label'));
@@ -3,6 +3,7 @@
3
3
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
4
  import type { TaskUserOverride } from '@tuturuuu/types';
5
5
  import { toast } from '@tuturuuu/ui/sonner';
6
+ import { getTaskApiUrl } from '../../../../../../lib/tasks-app-url';
6
7
 
7
8
  type OverrideInput = Partial<
8
9
  Omit<TaskUserOverride, 'task_id' | 'user_id' | 'created_at' | 'updated_at'>
@@ -23,7 +24,10 @@ export function useTaskOverrides(
23
24
  queryKey,
24
25
  queryFn: async (): Promise<TaskUserOverride | null> => {
25
26
  if (!taskId) return null;
26
- const res = await fetch(`/api/v1/users/me/tasks/${taskId}/overrides`);
27
+ const res = await fetch(
28
+ getTaskApiUrl(`/api/v1/users/me/tasks/${taskId}/overrides`),
29
+ { credentials: 'include' }
30
+ );
27
31
  if (!res.ok) throw new Error('Failed to fetch override');
28
32
  const json = await res.json();
29
33
  return json.data ?? null;
@@ -35,11 +39,15 @@ export function useTaskOverrides(
35
39
  const upsertMutation = useMutation({
36
40
  mutationFn: async (input: OverrideInput): Promise<TaskUserOverride> => {
37
41
  if (!taskId) throw new Error('No task ID');
38
- const res = await fetch(`/api/v1/users/me/tasks/${taskId}/overrides`, {
39
- method: 'PUT',
40
- headers: { 'Content-Type': 'application/json' },
41
- body: JSON.stringify(input),
42
- });
42
+ const res = await fetch(
43
+ getTaskApiUrl(`/api/v1/users/me/tasks/${taskId}/overrides`),
44
+ {
45
+ method: 'PUT',
46
+ credentials: 'include',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify(input),
49
+ }
50
+ );
43
51
  if (!res.ok) {
44
52
  const err = await res.json().catch(() => null);
45
53
  throw new Error(err?.error || 'Failed to save override');
@@ -85,9 +93,13 @@ export function useTaskOverrides(
85
93
  const deleteMutation = useMutation({
86
94
  mutationFn: async () => {
87
95
  if (!taskId) throw new Error('No task ID');
88
- const res = await fetch(`/api/v1/users/me/tasks/${taskId}/overrides`, {
89
- method: 'DELETE',
90
- });
96
+ const res = await fetch(
97
+ getTaskApiUrl(`/api/v1/users/me/tasks/${taskId}/overrides`),
98
+ {
99
+ method: 'DELETE',
100
+ credentials: 'include',
101
+ }
102
+ );
91
103
  if (!res.ok) throw new Error('Failed to delete override');
92
104
  },
93
105
  onMutate: async () => {
@@ -4,6 +4,7 @@ import {
4
4
  useQueryClient,
5
5
  } from '@tanstack/react-query';
6
6
  import type { Task } from '@tuturuuu/types/primitives/Task';
7
+ import { getTaskApiUrl } from '../../../../../../lib/tasks-app-url';
7
8
 
8
9
  export interface TaskUpdatePayload {
9
10
  name?: string;
@@ -33,11 +34,15 @@ export function useUpdateSharedTask(): UseMutationResult<
33
34
  shareCode: string;
34
35
  updates: TaskUpdatePayload;
35
36
  }) => {
36
- const response = await fetch(`/api/v1/shared/tasks/${shareCode}`, {
37
- method: 'PATCH',
38
- headers: { 'Content-Type': 'application/json' },
39
- body: JSON.stringify(updates),
40
- });
37
+ const response = await fetch(
38
+ getTaskApiUrl(`/api/v1/shared/tasks/${shareCode}`),
39
+ {
40
+ method: 'PATCH',
41
+ credentials: 'include',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify(updates),
44
+ }
45
+ );
41
46
 
42
47
  if (!response.ok) {
43
48
  const error = await response.json().catch(() => null);
@@ -35,6 +35,7 @@ import { usePathname } from 'next/navigation';
35
35
  import { useLocale, useTranslations } from 'next-intl';
36
36
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
37
37
  import * as Y from 'yjs';
38
+ import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
38
39
  import { BoardEstimationConfigDialog } from '../boards/boardId/task-dialogs/BoardEstimationConfigDialog';
39
40
  import { TaskNewLabelDialog } from '../boards/boardId/task-dialogs/TaskNewLabelDialog';
40
41
  import { TaskNewProjectDialog } from '../boards/boardId/task-dialogs/TaskNewProjectDialog';
@@ -391,8 +392,9 @@ export function TaskEditDialog({
391
392
  const { data: userTaskSettings } = useQuery({
392
393
  queryKey: ['user-task-settings'],
393
394
  queryFn: async () => {
394
- const res = await fetch('/api/v1/users/task-settings', {
395
+ const res = await fetch(getTaskApiUrl('/api/v1/users/task-settings'), {
395
396
  cache: 'no-store',
397
+ credentials: 'include',
396
398
  });
397
399
  if (!res.ok) return { task_auto_assign_to_self: false };
398
400
  return res.json() as Promise<{ task_auto_assign_to_self: boolean }>;
@@ -793,7 +795,7 @@ export function TaskEditDialog({
793
795
  enabled: !!isOpen && !isCreateMode && !!task?.id,
794
796
  queryFn: async () => {
795
797
  const response = await fetch(
796
- `/api/v1/users/me/tasks/${task!.id}/schedule`,
798
+ getTaskApiUrl(`/api/v1/users/me/tasks/${task!.id}/schedule`),
797
799
  { cache: 'no-store' }
798
800
  );
799
801
  if (!response.ok) return null;
@@ -17,6 +17,7 @@ import { Label } from '@tuturuuu/ui/label';
17
17
  import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
18
18
  import { cn } from '@tuturuuu/utils/format';
19
19
  import { useMemo, useState } from 'react';
20
+ import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
20
21
  import {
21
22
  buildEstimationIndices,
22
23
  mapEstimationPoints,
@@ -58,11 +59,13 @@ export function TaskEstimationPicker({
58
59
  queryFn: async () => {
59
60
  if (!boardId) return null;
60
61
  const response = await fetch(
61
- `/api/v1/workspaces/${wsId}/boards/${boardId}/estimation`
62
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/boards/${boardId}/estimation`)
62
63
  );
63
64
  if (!response.ok) {
64
65
  // If the endpoint doesn't exist, we'll create a fallback
65
- const boardResponse = await fetch(`/api/v1/workspaces/${wsId}/boards`);
66
+ const boardResponse = await fetch(
67
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/boards`)
68
+ );
66
69
  if (!boardResponse.ok) throw new Error('Failed to fetch boards');
67
70
  const boards = await boardResponse.json();
68
71
  const board = boards.find((b: any) => b.id === boardId);
@@ -180,7 +180,7 @@ export function TaskRowActionsMenu({
180
180
  canUseBoardAssignees ??
181
181
  (task.source_workspace_id ? true : !isPersonalWorkspace),
182
182
  assigneeMemberSource: task.source_workspace_id
183
- ? 'workspace'
183
+ ? 'board'
184
184
  : assigneeMemberSource,
185
185
  }
186
186
  );
@@ -41,6 +41,8 @@ vi.mock('@tuturuuu/internal-api/tasks', () => ({
41
41
  }));
42
42
 
43
43
  vi.mock('@tuturuuu/utils/task-helper', () => ({
44
+ isPersonalExternalStagingListId: (listId: string | null) =>
45
+ listId?.startsWith('personal-external-staging:') ?? false,
44
46
  useUpdateTask: vi.fn(),
45
47
  }));
46
48
 
@@ -1689,6 +1691,55 @@ describe('useTaskActions', () => {
1689
1691
  expect(setMenuOpen).toHaveBeenCalledWith(false);
1690
1692
  });
1691
1693
 
1694
+ it('moves a personal-workspace task with personal board metadata through the normal task route', async () => {
1695
+ const personalTask = {
1696
+ ...mockTask,
1697
+ personal_board_id: 'board-1',
1698
+ personal_list_id: 'list-1',
1699
+ source_workspace_id: 'ws-1',
1700
+ source_board_id: 'board-1',
1701
+ source_list_id: 'list-1',
1702
+ } as unknown as Task;
1703
+ const targetList = {
1704
+ id: 'list-2',
1705
+ name: 'Later',
1706
+ board_id: 'board-1',
1707
+ status: 'active',
1708
+ created_at: '2025-01-01T00:00:00Z',
1709
+ archived: false,
1710
+ deleted: false,
1711
+ creator_id: 'user-1',
1712
+ color: null,
1713
+ position: 3,
1714
+ } as unknown as TaskList;
1715
+
1716
+ queryClient.setQueryData(['tasks', 'board-1'], [personalTask]);
1717
+
1718
+ const { result } = renderHook(
1719
+ () =>
1720
+ useTaskActions({
1721
+ task: personalTask,
1722
+ boardId: 'board-1',
1723
+ targetCompletionList: mockCompletionList,
1724
+ targetClosedList: mockClosedList,
1725
+ availableLists: [...mockAvailableLists, targetList],
1726
+ onUpdate: vi.fn(),
1727
+ setIsLoading: vi.fn(),
1728
+ setMenuOpen: vi.fn(),
1729
+ }),
1730
+ { wrapper }
1731
+ );
1732
+
1733
+ await act(async () => {
1734
+ await result.current.handleMoveToList('list-2');
1735
+ });
1736
+
1737
+ expect(mockUpdateWorkspaceTask).toHaveBeenCalledWith('ws-1', 'task-1', {
1738
+ list_id: 'list-2',
1739
+ });
1740
+ expect(mockUpsertCurrentUserTaskPersonalPlacement).not.toHaveBeenCalled();
1741
+ });
1742
+
1692
1743
  it('moves an external task between personal lists through personal placement only', async () => {
1693
1744
  const targetList = {
1694
1745
  id: 'list-2',
@@ -11,11 +11,10 @@ import {
11
11
  isTaskBoardCompletedStatus,
12
12
  isTaskBoardTerminalStatus,
13
13
  } from '@tuturuuu/utils/task-list-status';
14
+ import { isPersonalExternalOverlayTask } from '../lib/task-personal-external';
14
15
 
15
16
  export function isPersonalExternalTask(task?: Task) {
16
- return (
17
- task?.is_personal_external === true || Boolean(task?.personal_board_id)
18
- );
17
+ return isPersonalExternalOverlayTask(task);
19
18
  }
20
19
 
21
20
  function findFirstListByStatus(lists: TaskList[], status: TaskBoardStatus) {