@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
@@ -1,7 +1,23 @@
1
1
  import { QueryClient } from '@tanstack/react-query';
2
2
  import type { Task } from '@tuturuuu/types/primitives/Task';
3
3
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
4
- import { describe, expect, it } from 'vitest';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+
6
+ vi.mock('@tuturuuu/utils/task-helper', () => {
7
+ const prefix = 'personal-external-staging:';
8
+
9
+ return {
10
+ getPersonalExternalStagingBoardId: (listId: string | null) =>
11
+ listId?.startsWith(prefix) && listId.length > prefix.length
12
+ ? listId.slice(prefix.length)
13
+ : null,
14
+ getPersonalExternalStagingListId: (boardId: string) =>
15
+ `${prefix}${boardId}`,
16
+ isPersonalExternalStagingListId: (listId: string | null) =>
17
+ Boolean(listId?.startsWith(prefix) && listId.length > prefix.length),
18
+ };
19
+ });
20
+
5
21
  import {
6
22
  applyTaskDropPreviewToCache,
7
23
  getProjectedTaskDropOrderFromPreview,
@@ -14,6 +30,7 @@ import {
14
30
  insertTaskAtDropPosition,
15
31
  mergePersonalPlacementMutationTask,
16
32
  mergeTaskIntoBoardTaskCache,
33
+ usesPersonalPlacement,
17
34
  } from './use-kanban-dnd';
18
35
 
19
36
  function createTask(overrides: Partial<Task> = {}): Task {
@@ -116,6 +133,51 @@ describe('mergePersonalPlacementMutationTask', () => {
116
133
  });
117
134
  });
118
135
 
136
+ describe('usesPersonalPlacement', () => {
137
+ it('does not treat a personal-workspace task as external from personal_board_id alone', () => {
138
+ const personalTask = createTask({
139
+ is_personal_external: false,
140
+ personal_board_id: 'personal-board',
141
+ personal_list_id: 'list-1',
142
+ source_board_id: null,
143
+ source_list_id: null,
144
+ source_workspace_id: null,
145
+ } as Partial<Task>);
146
+
147
+ expect(usesPersonalPlacement(personalTask)).toBe(false);
148
+ });
149
+
150
+ it('does not treat an explicitly native personal task as external when source metadata is present', () => {
151
+ const personalTask = createTask({
152
+ is_personal_external: false,
153
+ personal_board_id: 'personal-board',
154
+ personal_list_id: 'list-1',
155
+ source_workspace_id: 'personal-workspace',
156
+ source_board_id: 'personal-board',
157
+ source_list_id: 'list-1',
158
+ } as Partial<Task>);
159
+
160
+ expect(usesPersonalPlacement(personalTask)).toBe(false);
161
+ });
162
+
163
+ it('does not treat same-board personal metadata as external without an explicit flag', () => {
164
+ const personalTask = createTask({
165
+ is_personal_external: undefined,
166
+ personal_board_id: 'personal-board',
167
+ personal_list_id: 'list-1',
168
+ source_workspace_id: 'personal-workspace',
169
+ source_board_id: 'personal-board',
170
+ source_list_id: 'list-1',
171
+ } as Partial<Task>);
172
+
173
+ expect(usesPersonalPlacement(personalTask)).toBe(false);
174
+ });
175
+
176
+ it('keeps explicit external overlays on the personal-placement path', () => {
177
+ expect(usesPersonalPlacement(createTask())).toBe(true);
178
+ });
179
+ });
180
+
119
181
  describe('mergeTaskIntoBoardTaskCache', () => {
120
182
  it('updates an existing task in the board cache', () => {
121
183
  const existingTask = createTask({ list_id: 'source-list' });
@@ -21,6 +21,7 @@ import {
21
21
  } from '@tuturuuu/utils/task-helper';
22
22
  import { hasDraggableData } from '@tuturuuu/utils/task-helpers';
23
23
  import { useCallback, useRef, useState } from 'react';
24
+ import { isPersonalExternalOverlayTask } from '../../../../../../../lib/task-personal-external';
24
25
  import { useBoardBroadcast } from '../../../../shared/board-broadcast-context';
25
26
  import { invalidateKanbanDeadlineTasks } from '../data/kanban-deadline-query';
26
27
  import { MAX_SAFE_INTEGER_SORT } from '../kanban-constants';
@@ -121,12 +122,8 @@ interface UseKanbanDndProps {
121
122
  scrollContainerRef: React.RefObject<HTMLDivElement | null>;
122
123
  }
123
124
 
124
- function usesPersonalPlacement(task: Task) {
125
- return (
126
- task.is_personal_external === true ||
127
- Boolean(task.personal_board_id) ||
128
- isPersonalExternalStagingListId(task.list_id)
129
- );
125
+ export function usesPersonalPlacement(task: Task) {
126
+ return isPersonalExternalOverlayTask(task);
130
127
  }
131
128
 
132
129
  function getTaskDropPosition(
@@ -76,7 +76,7 @@ describe('getTaskCardHydratingOpenOptions', () => {
76
76
  taskWsId: 'source-workspace',
77
77
  taskWorkspacePersonal: false,
78
78
  canUseBoardAssignees: true,
79
- assigneeMemberSource: 'workspace',
79
+ assigneeMemberSource: 'board',
80
80
  initialSharedContext: {
81
81
  boardConfig: {
82
82
  id: 'source-board',
@@ -151,4 +151,14 @@ describe('getTaskCardHydratingOpenOptions', () => {
151
151
  ).toBe(true);
152
152
  expect(isExternalTaskSnapshot(task)).toBe(false);
153
153
  });
154
+
155
+ it('does not treat personal placement metadata alone as an external snapshot', () => {
156
+ expect(
157
+ isExternalTaskSnapshot({
158
+ ...task,
159
+ personal_board_id: 'board-1',
160
+ personal_list_id: 'list-1',
161
+ })
162
+ ).toBe(false);
163
+ });
154
164
  });
@@ -1,5 +1,6 @@
1
1
  import type { Task } from '@tuturuuu/types/primitives/Task';
2
2
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
3
+ import { isPersonalExternalOverlayTask } from '../../../../../../lib/task-personal-external';
3
4
  import type { SharedTaskContext } from '../../../shared/task-edit-dialog/hooks/use-task-data';
4
5
 
5
6
  interface TaskCardOpenOptionsInput {
@@ -13,9 +14,12 @@ interface TaskCardOpenOptionsInput {
13
14
  }
14
15
 
15
16
  export function isExternalTaskSnapshot(task: Task) {
17
+ if (task.is_personal_external === false) {
18
+ return false;
19
+ }
20
+
16
21
  return (
17
- task.is_personal_external === true ||
18
- Boolean(task.personal_board_id) ||
22
+ isPersonalExternalOverlayTask(task) ||
19
23
  Boolean(task.source_workspace_id) ||
20
24
  Boolean(task.source_board_id)
21
25
  );
@@ -131,7 +135,7 @@ export function getTaskCardHydratingOpenOptions({
131
135
  taskWorkspacePersonal: sourceWorkspaceId ? false : isPersonalWorkspace,
132
136
  canUseBoardAssignees: canUseBoardAssignees ?? sourceBoardAssigneesEnabled,
133
137
  assigneeMemberSource:
134
- assigneeMemberSource ?? (sourceWorkspaceId ? 'workspace' : undefined),
138
+ assigneeMemberSource ?? (sourceWorkspaceId ? 'board' : undefined),
135
139
  initialSharedContext,
136
140
  };
137
141
  }
@@ -0,0 +1,113 @@
1
+ import type { Task } from '@tuturuuu/types/primitives/Task';
2
+ import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { getTaskCardResourceContext } from './task-card-resource-context';
5
+
6
+ const pageList = {
7
+ id: 'page-list',
8
+ name: 'Page list',
9
+ board_id: 'page-board',
10
+ position: 0,
11
+ status: 'not_started',
12
+ color: 'BLUE',
13
+ created_at: '2026-07-04T00:00:00.000Z',
14
+ creator_id: 'user-1',
15
+ archived: false,
16
+ deleted: false,
17
+ } satisfies TaskList;
18
+
19
+ const task = {
20
+ id: 'task-1',
21
+ name: 'Task',
22
+ description: '',
23
+ list_id: 'page-list',
24
+ display_number: 1,
25
+ created_at: '2026-07-04T00:00:00.000Z',
26
+ end_date: null,
27
+ priority: 'normal',
28
+ } satisfies Task;
29
+
30
+ describe('getTaskCardResourceContext', () => {
31
+ it('keeps local task resources on the page workspace and board', () => {
32
+ expect(
33
+ getTaskCardResourceContext({
34
+ boardId: 'page-board',
35
+ pageWorkspaceId: 'page-workspace',
36
+ propAvailableLists: [pageList],
37
+ task,
38
+ })
39
+ ).toEqual({
40
+ boardViewableMembersBoardId: 'page-board',
41
+ boardViewableMembersWorkspaceId: 'page-workspace',
42
+ effectiveWorkspaceId: 'page-workspace',
43
+ initialAvailableLists: [pageList],
44
+ isSourceWorkspaceTask: false,
45
+ taskBoardId: 'page-board',
46
+ });
47
+ });
48
+
49
+ it('routes source task labels, projects, lists, and members to the source workspace and board', () => {
50
+ expect(
51
+ getTaskCardResourceContext({
52
+ boardId: 'personal-board',
53
+ pageWorkspaceId: 'personal-workspace',
54
+ propAvailableLists: [pageList],
55
+ task: {
56
+ ...task,
57
+ list_id: 'personal-list',
58
+ source_board_id: 'source-board',
59
+ source_workspace_id: 'source-workspace',
60
+ },
61
+ })
62
+ ).toEqual({
63
+ boardViewableMembersBoardId: 'source-board',
64
+ boardViewableMembersWorkspaceId: 'source-workspace',
65
+ effectiveWorkspaceId: 'source-workspace',
66
+ initialAvailableLists: undefined,
67
+ isSourceWorkspaceTask: true,
68
+ taskBoardId: 'source-board',
69
+ });
70
+ });
71
+
72
+ it('uses the page workspace when a source board is present without source workspace metadata', () => {
73
+ expect(
74
+ getTaskCardResourceContext({
75
+ boardId: 'page-board',
76
+ pageWorkspaceId: 'page-workspace',
77
+ propAvailableLists: [pageList],
78
+ task: {
79
+ ...task,
80
+ source_board_id: 'source-board',
81
+ },
82
+ })
83
+ ).toMatchObject({
84
+ boardViewableMembersBoardId: 'source-board',
85
+ boardViewableMembersWorkspaceId: 'page-workspace',
86
+ effectiveWorkspaceId: 'page-workspace',
87
+ initialAvailableLists: undefined,
88
+ isSourceWorkspaceTask: true,
89
+ taskBoardId: 'source-board',
90
+ });
91
+ });
92
+
93
+ it('keeps page board list data when only source workspace metadata is available', () => {
94
+ expect(
95
+ getTaskCardResourceContext({
96
+ boardId: 'page-board',
97
+ pageWorkspaceId: 'page-workspace',
98
+ propAvailableLists: [pageList],
99
+ task: {
100
+ ...task,
101
+ source_workspace_id: 'source-workspace',
102
+ },
103
+ })
104
+ ).toMatchObject({
105
+ boardViewableMembersBoardId: 'page-board',
106
+ boardViewableMembersWorkspaceId: 'source-workspace',
107
+ effectiveWorkspaceId: 'source-workspace',
108
+ initialAvailableLists: [pageList],
109
+ isSourceWorkspaceTask: true,
110
+ taskBoardId: 'page-board',
111
+ });
112
+ });
113
+ });
@@ -0,0 +1,50 @@
1
+ import type { Task } from '@tuturuuu/types/primitives/Task';
2
+ import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
3
+
4
+ export interface TaskCardResourceContextInput {
5
+ boardId: string;
6
+ pageWorkspaceId?: string;
7
+ propAvailableLists?: TaskList[];
8
+ task: Task;
9
+ }
10
+
11
+ export interface TaskCardResourceContext {
12
+ boardViewableMembersBoardId: string;
13
+ boardViewableMembersWorkspaceId?: string;
14
+ effectiveWorkspaceId?: string;
15
+ initialAvailableLists?: TaskList[];
16
+ isSourceWorkspaceTask: boolean;
17
+ taskBoardId: string;
18
+ }
19
+
20
+ export function getTaskCardResourceContext({
21
+ boardId,
22
+ pageWorkspaceId,
23
+ propAvailableLists,
24
+ task,
25
+ }: TaskCardResourceContextInput): TaskCardResourceContext {
26
+ const effectiveWorkspaceId = task.source_workspace_id ?? pageWorkspaceId;
27
+ const taskBoardId = task.source_board_id ?? boardId;
28
+ const isSourceWorkspaceTask = Boolean(
29
+ task.source_workspace_id || task.source_board_id
30
+ );
31
+ const boardViewableMembersWorkspaceId =
32
+ task.source_workspace_id ?? pageWorkspaceId;
33
+ const boardViewableMembersBoardId =
34
+ isSourceWorkspaceTask && task.source_board_id
35
+ ? task.source_board_id
36
+ : boardId;
37
+ const initialAvailableLists =
38
+ isSourceWorkspaceTask && task.source_board_id
39
+ ? undefined
40
+ : propAvailableLists;
41
+
42
+ return {
43
+ boardViewableMembersBoardId,
44
+ boardViewableMembersWorkspaceId,
45
+ effectiveWorkspaceId,
46
+ initialAvailableLists,
47
+ isSourceWorkspaceTask,
48
+ taskBoardId,
49
+ };
50
+ }
@@ -33,6 +33,7 @@ import {
33
33
  listWorkspaceTaskBoardViewableMembers,
34
34
  listWorkspaceTaskLists,
35
35
  listWorkspaceTaskProjects,
36
+ listWorkspaceTaskProjectsByIds,
36
37
  removeCurrentUserTaskPersonalPlacement,
37
38
  } from '@tuturuuu/internal-api/tasks';
38
39
  import type { SupportedColor } from '@tuturuuu/types/primitives/SupportedColors';
@@ -85,6 +86,7 @@ import React, {
85
86
  useRef,
86
87
  useState,
87
88
  } from 'react';
89
+ import { isPersonalExternalOverlayTask } from '../../../../../../lib/task-personal-external';
88
90
  import {
89
91
  enqueueTaskCardRelationshipRequest,
90
92
  useTaskCardRelationships,
@@ -153,6 +155,7 @@ import {
153
155
  getTaskCardHydratingOpenOptions,
154
156
  isExternalTaskSnapshot,
155
157
  } from './task-card-open-options';
158
+ import { getTaskCardResourceContext } from './task-card-resource-context';
156
159
  import { getTaskCardVisibilityState } from './task-card-visibility';
157
160
  import { TaskSchedulingBadge } from './task-scheduling-badge';
158
161
 
@@ -390,7 +393,24 @@ function TaskCardInner({
390
393
  // Use React Query hooks for shared data (cached across all task cards)
391
394
  const workspaceContextWsId = workspaceId ?? wsId;
392
395
  const { data: boardConfig } = useBoardConfig(boardId, workspaceContextWsId);
393
- const effectiveWorkspaceId = workspaceId ?? boardConfig?.ws_id ?? wsId;
396
+ const pageWorkspaceId = workspaceId ?? boardConfig?.ws_id ?? wsId;
397
+ const {
398
+ boardViewableMembersBoardId,
399
+ boardViewableMembersWorkspaceId,
400
+ effectiveWorkspaceId,
401
+ initialAvailableLists,
402
+ isSourceWorkspaceTask,
403
+ taskBoardId,
404
+ } = getTaskCardResourceContext({
405
+ boardId,
406
+ pageWorkspaceId,
407
+ propAvailableLists,
408
+ task,
409
+ });
410
+ const taskProjectIds = useMemo(
411
+ () => task.projects?.map((project) => project.id).filter(Boolean) ?? [],
412
+ [task.projects]
413
+ );
394
414
  const taskShareWsId = effectiveWorkspaceId;
395
415
  const { data: workspaceLabels = [], isLoading: labelsLoading } =
396
416
  useWorkspaceLabels(effectiveWorkspaceId);
@@ -431,12 +451,24 @@ function TaskCardInner({
431
451
  // Fetch workspace projects
432
452
  const { data: workspaceProjects = [], isLoading: projectsLoading } = useQuery(
433
453
  {
434
- queryKey: ['task_projects', effectiveWorkspaceId],
454
+ queryKey: [
455
+ 'task_projects',
456
+ effectiveWorkspaceId,
457
+ isSourceWorkspaceTask ? taskProjectIds.join(',') : 'all',
458
+ ],
435
459
  queryFn: async () => {
436
460
  if (!effectiveWorkspaceId) return [];
461
+ if (isSourceWorkspaceTask) {
462
+ return listWorkspaceTaskProjectsByIds(
463
+ effectiveWorkspaceId,
464
+ taskProjectIds
465
+ );
466
+ }
437
467
  return listWorkspaceTaskProjects(effectiveWorkspaceId);
438
468
  },
439
- enabled: !!effectiveWorkspaceId,
469
+ enabled:
470
+ !!effectiveWorkspaceId &&
471
+ (!isSourceWorkspaceTask || taskProjectIds.length > 0),
440
472
  staleTime: 5 * 60 * 1000, // 5 minutes - projects rarely change
441
473
  }
442
474
  );
@@ -470,27 +502,35 @@ function TaskCardInner({
470
502
  enabled: !!effectiveWorkspaceId && shouldLoadWorkspaceMembers,
471
503
  });
472
504
  const boardViewableMembersQuery = useQuery({
473
- queryKey: ['task-board-viewable-members', effectiveWorkspaceId, boardId],
505
+ queryKey: [
506
+ 'task-board-viewable-members',
507
+ boardViewableMembersWorkspaceId,
508
+ boardViewableMembersBoardId,
509
+ ],
474
510
  queryFn: async (): Promise<WorkspaceMember[]> => {
475
- if (!effectiveWorkspaceId || !boardId) return [];
511
+ if (!boardViewableMembersWorkspaceId || !boardViewableMembersBoardId) {
512
+ return [];
513
+ }
476
514
 
477
515
  const payload = await listWorkspaceTaskBoardViewableMembers(
478
- effectiveWorkspaceId,
479
- boardId
516
+ boardViewableMembersWorkspaceId,
517
+ boardViewableMembersBoardId
480
518
  );
481
519
  const members = Array.isArray(payload?.members) ? payload.members : [];
482
520
 
483
521
  return members.map((member) => ({
484
522
  id: member.user_id,
485
523
  user_id: member.user_id,
486
- workspace_id: effectiveWorkspaceId,
524
+ workspace_id: boardViewableMembersWorkspaceId,
487
525
  display_name: member.display_name ?? member.email ?? member.user_id,
488
526
  email: member.email ?? undefined,
489
527
  avatar_url: member.avatar_url ?? undefined,
490
528
  }));
491
529
  },
492
530
  enabled:
493
- !!effectiveWorkspaceId && !!boardId && shouldLoadBoardViewableMembers,
531
+ !!boardViewableMembersWorkspaceId &&
532
+ !!boardViewableMembersBoardId &&
533
+ shouldLoadBoardViewableMembers,
494
534
  staleTime: 5 * 60 * 1000,
495
535
  });
496
536
  const normalWorkspaceMembers = normalMembersQuery.data ?? [];
@@ -717,15 +757,15 @@ function TaskCardInner({
717
757
 
718
758
  // Fetch available task lists using React Query (same key as other components)
719
759
  const { data: availableLists = [] } = useQuery({
720
- queryKey: ['task_lists', boardId],
760
+ queryKey: ['task_lists', taskBoardId],
721
761
  queryFn: async () => {
722
- if (!effectiveWorkspaceId) {
762
+ if (!effectiveWorkspaceId || !taskBoardId) {
723
763
  return [];
724
764
  }
725
765
 
726
766
  const { lists } = await listWorkspaceTaskLists(
727
767
  effectiveWorkspaceId,
728
- boardId,
768
+ taskBoardId,
729
769
  typeof window !== 'undefined'
730
770
  ? { baseUrl: window.location.origin }
731
771
  : undefined
@@ -733,8 +773,8 @@ function TaskCardInner({
733
773
 
734
774
  return lists.filter((list) => !list.deleted) as TaskList[];
735
775
  },
736
- enabled: !propAvailableLists && !!effectiveWorkspaceId, // Only fetch if not provided as prop
737
- initialData: propAvailableLists,
776
+ enabled: !initialAvailableLists && !!effectiveWorkspaceId && !!taskBoardId,
777
+ initialData: initialAvailableLists,
738
778
  staleTime: 60 * 1000, // 1 minute - lists change less frequently
739
779
  });
740
780
 
@@ -852,6 +892,7 @@ function TaskCardInner({
852
892
  // Check if task is optimistically added (pending realtime confirmation)
853
893
  const isOptimistic = '_isOptimistic' in task && task._isOptimistic === true;
854
894
  const isPersonalExternalTask = isExternalTaskSnapshot(task);
895
+ const isPersonalExternalOverlay = isPersonalExternalOverlayTask(task);
855
896
  const sourceBoardUrl =
856
897
  task.source_workspace_id && task.source_board_id
857
898
  ? `/${task.source_workspace_id}${tasksHref(`/boards/${task.source_board_id}`)}`
@@ -1098,7 +1139,7 @@ function TaskCardInner({
1098
1139
  ? true
1099
1140
  : shouldUseBoardAssignees,
1100
1141
  assigneeMemberSource: task.source_workspace_id
1101
- ? 'workspace'
1142
+ ? 'board'
1102
1143
  : effectiveAssigneeMemberSource,
1103
1144
  effectiveWorkspaceId,
1104
1145
  isPersonalWorkspace,
@@ -1201,7 +1242,13 @@ function TaskCardInner({
1201
1242
  const handleAddSubtask = () => {
1202
1243
  setMenuOpen(false);
1203
1244
  // Open subtask creation dialog - the relationship will be created when the user saves
1204
- createSubtask(task.id, task.name, boardId, task.list_id, availableLists);
1245
+ createSubtask(
1246
+ task.id,
1247
+ task.name,
1248
+ taskBoardId,
1249
+ task.list_id,
1250
+ availableLists
1251
+ );
1205
1252
  };
1206
1253
 
1207
1254
  const handleMoveToExternalStaging = async () => {
@@ -2303,7 +2350,7 @@ function TaskCardInner({
2303
2350
  />
2304
2351
  )}
2305
2352
 
2306
- {isPersonalExternalTask && (
2353
+ {isPersonalExternalOverlay && (
2307
2354
  <>
2308
2355
  {task.personal_board_id && task.personal_list_id && (
2309
2356
  <DropdownMenuItem
@@ -2715,7 +2762,7 @@ function TaskCardInner({
2715
2762
  <CreateListDialog
2716
2763
  open={isCreateListDialogOpen}
2717
2764
  onOpenChange={setIsCreateListDialogOpen}
2718
- boardId={boardId}
2765
+ boardId={taskBoardId}
2719
2766
  wsId={effectiveWorkspaceId}
2720
2767
  initialStatus="active"
2721
2768
  onSuccess={(listId) => {
@@ -254,7 +254,7 @@ export function TimelineBoard({
254
254
  ? true
255
255
  : showAssignees,
256
256
  assigneeMemberSource: task.source_workspace_id
257
- ? 'workspace'
257
+ ? 'board'
258
258
  : effectiveAssigneeMemberSource,
259
259
  })
260
260
  );
@@ -47,6 +47,7 @@ import { toast } from '@tuturuuu/ui/sonner';
47
47
  import { Textarea } from '@tuturuuu/ui/textarea';
48
48
  import dayjs from 'dayjs';
49
49
  import { useCallback, useEffect, useMemo, useState } from 'react';
50
+ import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
50
51
 
51
52
  type CycleStatus = 'planned' | 'active' | 'completed' | 'cancelled';
52
53
 
@@ -128,7 +129,9 @@ export function TaskCyclesClient({
128
129
  } = useQuery<TaskCycle[]>({
129
130
  queryKey: ['workspace', wsId, 'task-cycles'],
130
131
  queryFn: async () => {
131
- const response = await fetch(`/api/v1/workspaces/${wsId}/task-cycles`);
132
+ const response = await fetch(
133
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/task-cycles`)
134
+ );
132
135
  if (!response.ok) throw new Error('Failed to fetch cycles');
133
136
  return response.json();
134
137
  },
@@ -149,17 +152,21 @@ export function TaskCyclesClient({
149
152
  start_date?: string | null;
150
153
  end_date?: string | null;
151
154
  }) => {
152
- const response = await fetch(`/api/v1/workspaces/${wsId}/task-cycles`, {
153
- method: 'POST',
154
- headers: { 'Content-Type': 'application/json' },
155
- body: JSON.stringify({
156
- name,
157
- description,
158
- status,
159
- start_date,
160
- end_date,
161
- }),
162
- });
155
+ const response = await fetch(
156
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/task-cycles`),
157
+ {
158
+ method: 'POST',
159
+ credentials: 'include',
160
+ headers: { 'Content-Type': 'application/json' },
161
+ body: JSON.stringify({
162
+ name,
163
+ description,
164
+ status,
165
+ start_date,
166
+ end_date,
167
+ }),
168
+ }
169
+ );
163
170
  if (!response.ok) {
164
171
  const data = await response.json();
165
172
  throw new Error(data.error || 'Failed to create cycle');
@@ -196,9 +203,10 @@ export function TaskCyclesClient({
196
203
  end_date?: string | null;
197
204
  }) => {
198
205
  const response = await fetch(
199
- `/api/v1/workspaces/${wsId}/task-cycles/${id}`,
206
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/task-cycles/${id}`),
200
207
  {
201
208
  method: 'PUT',
209
+ credentials: 'include',
202
210
  headers: { 'Content-Type': 'application/json' },
203
211
  body: JSON.stringify({
204
212
  name,
@@ -232,9 +240,10 @@ export function TaskCyclesClient({
232
240
  const deleteMutation = useMutation({
233
241
  mutationFn: async (id: string) => {
234
242
  const response = await fetch(
235
- `/api/v1/workspaces/${wsId}/task-cycles/${id}`,
243
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/task-cycles/${id}`),
236
244
  {
237
245
  method: 'DELETE',
246
+ credentials: 'include',
238
247
  }
239
248
  );
240
249
  if (!response.ok) {
@@ -3,6 +3,7 @@
3
3
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
4
  import type { WorkspaceTaskBoard } from '@tuturuuu/types';
5
5
  import { useTranslations } from 'next-intl';
6
+ import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
6
7
 
7
8
  export interface TaskEstimateBoardsResponse {
8
9
  boards: Partial<WorkspaceTaskBoard>[];
@@ -34,9 +35,12 @@ export const taskEstimateBoardKeys = {
34
35
  export async function fetchTaskEstimateBoards(
35
36
  wsId: string
36
37
  ): Promise<TaskEstimateBoardsResponse> {
37
- const response = await fetch(`/api/v1/workspaces/${wsId}/boards/estimation`, {
38
- cache: 'no-store',
39
- });
38
+ const response = await fetch(
39
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/boards/estimation`),
40
+ {
41
+ cache: 'no-store',
42
+ }
43
+ );
40
44
 
41
45
  if (!response.ok) {
42
46
  throw new Error('Failed to fetch task estimate boards');
@@ -56,9 +60,10 @@ async function updateTaskEstimateBoard(
56
60
  }: UpdateTaskEstimateBoardInput
57
61
  ) {
58
62
  const response = await fetch(
59
- `/api/v1/workspaces/${wsId}/boards/${boardId}/estimation`,
63
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/boards/${boardId}/estimation`),
60
64
  {
61
65
  cache: 'no-store',
66
+ credentials: 'include',
62
67
  method: 'PATCH',
63
68
  headers: {
64
69
  'Content-Type': 'application/json',
@@ -14,6 +14,7 @@ import {
14
14
  import { Skeleton } from '@tuturuuu/ui/skeleton';
15
15
  import { toast } from '@tuturuuu/ui/sonner';
16
16
  import { useState } from 'react';
17
+ import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
17
18
  import HabitCard from './habit-card';
18
19
  import HabitFormDialog from './habit-form-dialog';
19
20
 
@@ -39,7 +40,9 @@ export default function HabitsClientPage({ wsId }: HabitsClientPageProps) {
39
40
  } = useQuery({
40
41
  queryKey: ['habits', wsId],
41
42
  queryFn: async () => {
42
- const response = await fetch(`/api/v1/workspaces/${wsId}/habits`);
43
+ const response = await fetch(
44
+ getTaskApiUrl(`/api/v1/workspaces/${wsId}/habits`)
45
+ );
43
46
  if (!response.ok) {
44
47
  throw new Error('Failed to fetch habits');
45
48
  }