@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.
- package/CHANGELOG.md +23 -0
- package/package.json +5 -5
- package/src/components/ui/calendar-app/components/habits-panel.tsx +5 -2
- package/src/components/ui/calendar-app/components/priority-view.tsx +15 -7
- package/src/components/ui/calendar-app/components/scheduling-dialog.tsx +7 -3
- package/src/components/ui/calendar-app/components/task-scheduler-panel.tsx +7 -3
- package/src/components/ui/calendar-app/components/time-tracker/index.tsx +14 -9
- package/src/components/ui/custom/settings/task-settings.tsx +6 -2
- package/src/components/ui/custom/tables/data-table.tsx +58 -8
- package/src/components/ui/storefront/bundle-selection-dialog.tsx +249 -0
- package/src/components/ui/storefront/cart-summary-parts.tsx +28 -8
- package/src/components/ui/storefront/listing-card.tsx +6 -3
- package/src/components/ui/storefront/storefront-surface.test.tsx +134 -1
- package/src/components/ui/storefront/storefront-surface.tsx +66 -7
- package/src/components/ui/storefront/types.ts +15 -0
- package/src/components/ui/storefront/utils.ts +164 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +3 -6
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +11 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -3
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-resource-context.test.ts +113 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-resource-context.ts +50 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +65 -18
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +1 -1
- package/src/components/ui/tu-do/cycles/task-cycles-client.tsx +23 -14
- package/src/components/ui/tu-do/estimates/use-task-estimates.ts +9 -4
- package/src/components/ui/tu-do/habits/client.tsx +4 -1
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +6 -3
- package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +19 -11
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-query.ts +15 -6
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +18 -13
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +17 -10
- package/src/components/ui/tu-do/shared/fade-setting-initializer.tsx +3 -1
- package/src/components/ui/tu-do/shared/list-view.tsx +1 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +6 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-update-shared-task.test.ts +9 -5
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +13 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-overrides.ts +21 -9
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-update-shared-task.ts +10 -5
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +4 -2
- package/src/components/ui/tu-do/shared/task-estimation-picker.tsx +5 -2
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +1 -1
- package/src/hooks/__tests__/use-task-actions.test.tsx +51 -0
- package/src/hooks/task-actions-personal-external.ts +2 -3
- package/src/hooks/use-board-actions.ts +27 -17
- package/src/hooks/use-calendar.tsx +7 -3
- package/src/hooks/use-settings-dialog-shortcut.ts +51 -0
- package/src/lib/task-personal-external.ts +49 -0
- 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: '
|
|
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
|
|
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 ? '
|
|
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
|
|
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: [
|
|
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:
|
|
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: [
|
|
505
|
+
queryKey: [
|
|
506
|
+
'task-board-viewable-members',
|
|
507
|
+
boardViewableMembersWorkspaceId,
|
|
508
|
+
boardViewableMembersBoardId,
|
|
509
|
+
],
|
|
474
510
|
queryFn: async (): Promise<WorkspaceMember[]> => {
|
|
475
|
-
if (!
|
|
511
|
+
if (!boardViewableMembersWorkspaceId || !boardViewableMembersBoardId) {
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
476
514
|
|
|
477
515
|
const payload = await listWorkspaceTaskBoardViewableMembers(
|
|
478
|
-
|
|
479
|
-
|
|
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:
|
|
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
|
-
!!
|
|
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',
|
|
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
|
-
|
|
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: !
|
|
737
|
-
initialData:
|
|
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
|
-
? '
|
|
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(
|
|
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
|
-
{
|
|
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={
|
|
2765
|
+
boardId={taskBoardId}
|
|
2719
2766
|
wsId={effectiveWorkspaceId}
|
|
2720
2767
|
initialStatus="active"
|
|
2721
2768
|
onSuccess={(listId) => {
|
|
@@ -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(
|
|
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(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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(
|
|
38
|
-
|
|
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(
|
|
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
|
}
|