@tuturuuu/ui 0.12.0 → 0.14.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 +28 -0
- package/package.json +4 -4
- 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 +61 -2
- package/src/components/ui/custom/tables/data-table.tsx +58 -8
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.test.ts +118 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.ts +144 -70
- 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 +21 -10
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +6 -0
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +4 -2
- 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/__tests__/board-views.test.tsx +70 -0
- package/src/components/ui/tu-do/shared/board-views.tsx +33 -3
- 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-quick-create-target-list.ts +20 -0
- 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/hooks/use-user-config.ts +10 -2
- package/src/lib/task-personal-external.ts +49 -0
- package/src/lib/tasks-app-url.ts +85 -0
|
@@ -17,7 +17,9 @@ import { getBoardConfigKey } from '../board-config-storage';
|
|
|
17
17
|
import { BoardViews } from '../board-views';
|
|
18
18
|
|
|
19
19
|
const listWorkspaceTasksMock = vi.hoisted(() => vi.fn());
|
|
20
|
+
const getUserConfigMock = vi.hoisted(() => vi.fn());
|
|
20
21
|
const getUserWorkspaceConfigMock = vi.hoisted(() => vi.fn());
|
|
22
|
+
const updateUserConfigMock = vi.hoisted(() => vi.fn());
|
|
21
23
|
const updateUserWorkspaceConfigMock = vi.hoisted(() => vi.fn());
|
|
22
24
|
const createTaskMock = vi.fn();
|
|
23
25
|
const loadListPageMock = vi.fn();
|
|
@@ -64,8 +66,11 @@ vi.mock('@tuturuuu/internal-api/tasks', () => ({
|
|
|
64
66
|
vi.mock('@tuturuuu/internal-api/users', () => ({
|
|
65
67
|
TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID: 'TASK_BOARD_PINNED_SPECIAL_LISTS',
|
|
66
68
|
TASK_LAST_BOARD_VIEW_CONFIG_ID: 'TASK_LAST_BOARD_VIEW',
|
|
69
|
+
TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID: 'TASK_QUICK_CREATE_TARGET_LIST',
|
|
70
|
+
getUserConfig: (...args: unknown[]) => getUserConfigMock(...args),
|
|
67
71
|
getUserWorkspaceConfig: (...args: unknown[]) =>
|
|
68
72
|
getUserWorkspaceConfigMock(...args),
|
|
73
|
+
updateUserConfig: (...args: unknown[]) => updateUserConfigMock(...args),
|
|
69
74
|
updateUserWorkspaceConfig: (...args: unknown[]) =>
|
|
70
75
|
updateUserWorkspaceConfigMock(...args),
|
|
71
76
|
}));
|
|
@@ -260,8 +265,12 @@ describe('BoardViews', () => {
|
|
|
260
265
|
progressivePagination = {};
|
|
261
266
|
listWorkspaceTasksMock.mockReset();
|
|
262
267
|
listWorkspaceTasksMock.mockResolvedValue({ tasks: mockTasks });
|
|
268
|
+
getUserConfigMock.mockReset();
|
|
269
|
+
getUserConfigMock.mockResolvedValue({ value: null });
|
|
263
270
|
getUserWorkspaceConfigMock.mockReset();
|
|
264
271
|
getUserWorkspaceConfigMock.mockResolvedValue({ value: null });
|
|
272
|
+
updateUserConfigMock.mockReset();
|
|
273
|
+
updateUserConfigMock.mockResolvedValue({ message: 'ok' });
|
|
265
274
|
updateUserWorkspaceConfigMock.mockReset();
|
|
266
275
|
updateUserWorkspaceConfigMock.mockResolvedValue({ message: 'ok' });
|
|
267
276
|
window.localStorage.clear();
|
|
@@ -312,6 +321,7 @@ describe('BoardViews', () => {
|
|
|
312
321
|
expect(boardHeaderProps?.titlePrefix).toBeDefined();
|
|
313
322
|
expect(kanbanBoardProps?.readOnly).toBe(true);
|
|
314
323
|
expect(listWorkspaceTasksMock).not.toHaveBeenCalled();
|
|
324
|
+
expect(getUserConfigMock).not.toHaveBeenCalled();
|
|
315
325
|
});
|
|
316
326
|
|
|
317
327
|
it('enables assignees for personal boards that have guest access', async () => {
|
|
@@ -544,6 +554,66 @@ describe('BoardViews', () => {
|
|
|
544
554
|
);
|
|
545
555
|
});
|
|
546
556
|
|
|
557
|
+
it('creates a task in the hovered list when the quick-create target prefers hovered lists', async () => {
|
|
558
|
+
getUserConfigMock.mockResolvedValue({ value: 'hovered_list' });
|
|
559
|
+
const { queryClient } = renderBoardViews({
|
|
560
|
+
board: { ...mockBoard, default_list_id: 'list-1' },
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await waitFor(() => {
|
|
564
|
+
expect(
|
|
565
|
+
queryClient.getQueryData([
|
|
566
|
+
'user-config',
|
|
567
|
+
'TASK_QUICK_CREATE_TARGET_LIST',
|
|
568
|
+
])
|
|
569
|
+
).toBe('hovered_list');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
await act(async () => {
|
|
573
|
+
kanbanBoardProps?.onHoveredTaskListChange?.('list-2');
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
fireEvent.keyDown(document, { key: 'c' });
|
|
577
|
+
|
|
578
|
+
expect(createTaskMock).toHaveBeenCalledTimes(1);
|
|
579
|
+
expect(createTaskMock).toHaveBeenCalledWith(
|
|
580
|
+
'board-1',
|
|
581
|
+
'list-2',
|
|
582
|
+
mockLists,
|
|
583
|
+
expect.objectContaining({ labels: [] })
|
|
584
|
+
);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('falls back to the board default list in hovered-list mode when no hovered list is selectable', async () => {
|
|
588
|
+
getUserConfigMock.mockResolvedValue({ value: 'hovered_list' });
|
|
589
|
+
const { queryClient } = renderBoardViews({
|
|
590
|
+
board: { ...mockBoard, default_list_id: 'list-2' },
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await waitFor(() => {
|
|
594
|
+
expect(
|
|
595
|
+
queryClient.getQueryData([
|
|
596
|
+
'user-config',
|
|
597
|
+
'TASK_QUICK_CREATE_TARGET_LIST',
|
|
598
|
+
])
|
|
599
|
+
).toBe('hovered_list');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
await act(async () => {
|
|
603
|
+
kanbanBoardProps?.onHoveredTaskListChange?.('external-list');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
fireEvent.keyDown(document, { key: 'c' });
|
|
607
|
+
|
|
608
|
+
expect(createTaskMock).toHaveBeenCalledTimes(1);
|
|
609
|
+
expect(createTaskMock).toHaveBeenCalledWith(
|
|
610
|
+
'board-1',
|
|
611
|
+
'list-2',
|
|
612
|
+
mockLists,
|
|
613
|
+
expect.objectContaining({ labels: [] })
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
|
|
547
617
|
it('falls back to the first list when the default list is unavailable', () => {
|
|
548
618
|
renderBoardViews({
|
|
549
619
|
board: { ...mockBoard, default_list_id: 'list-does-not-exist' },
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
|
|
15
15
|
TASK_LAST_BOARD_VIEW_CONFIG_ID,
|
|
16
|
+
TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID,
|
|
16
17
|
} from '@tuturuuu/internal-api/users';
|
|
17
18
|
import type {
|
|
18
19
|
Workspace,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
useMemo,
|
|
36
37
|
useState,
|
|
37
38
|
} from 'react';
|
|
39
|
+
import { useUserConfig } from '../../../../hooks/use-user-config';
|
|
38
40
|
import {
|
|
39
41
|
useUpdateUserWorkspaceConfig,
|
|
40
42
|
useUserWorkspaceConfig,
|
|
@@ -58,6 +60,10 @@ import {
|
|
|
58
60
|
type SpecialTaskListPin,
|
|
59
61
|
serializeSpecialTaskListPins,
|
|
60
62
|
} from './special-task-list-pins';
|
|
63
|
+
import {
|
|
64
|
+
DEFAULT_TASK_QUICK_CREATE_TARGET_LIST,
|
|
65
|
+
normalizeTaskQuickCreateTargetList,
|
|
66
|
+
} from './task-quick-create-target-list';
|
|
61
67
|
|
|
62
68
|
export type ViewType =
|
|
63
69
|
| 'kanban'
|
|
@@ -290,8 +296,19 @@ export function BoardViews({
|
|
|
290
296
|
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false);
|
|
291
297
|
const [kanbanBulkSelectionActive, setKanbanBulkSelectionActive] =
|
|
292
298
|
useState(false);
|
|
299
|
+
const [hoveredTaskListId, setHoveredTaskListId] = useState<string | null>(
|
|
300
|
+
null
|
|
301
|
+
);
|
|
293
302
|
const { createTask } = useTaskDialog();
|
|
294
303
|
const localTaskState = readOnly || publicView;
|
|
304
|
+
const { data: quickCreateTargetListRaw } = useUserConfig(
|
|
305
|
+
TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID,
|
|
306
|
+
DEFAULT_TASK_QUICK_CREATE_TARGET_LIST,
|
|
307
|
+
{ enabled: !localTaskState }
|
|
308
|
+
);
|
|
309
|
+
const quickCreateTargetList = normalizeTaskQuickCreateTargetList(
|
|
310
|
+
quickCreateTargetListRaw
|
|
311
|
+
);
|
|
295
312
|
const boardAssigneesEnabled =
|
|
296
313
|
!workspace.personal ||
|
|
297
314
|
board.access_type === 'guest' ||
|
|
@@ -820,6 +837,15 @@ export function BoardViews({
|
|
|
820
837
|
statusFilteredLists,
|
|
821
838
|
]);
|
|
822
839
|
|
|
840
|
+
useEffect(() => {
|
|
841
|
+
if (!hoveredTaskListId) return;
|
|
842
|
+
|
|
843
|
+
const stillSelectable = filteredLists.some(
|
|
844
|
+
(list) => list.id === hoveredTaskListId && !list.is_external_staging
|
|
845
|
+
);
|
|
846
|
+
if (!stillSelectable) setHoveredTaskListId(null);
|
|
847
|
+
}, [filteredLists, hoveredTaskListId]);
|
|
848
|
+
|
|
823
849
|
const sourceTasks = useMemo(() => {
|
|
824
850
|
if (localTaskState) return locallyFilteredTasks;
|
|
825
851
|
|
|
@@ -902,11 +928,14 @@ export function BoardViews({
|
|
|
902
928
|
const selectableLists = filteredLists.filter(
|
|
903
929
|
(list) => !list.is_external_staging
|
|
904
930
|
);
|
|
905
|
-
|
|
906
|
-
// to the first selectable list when unset or the list is unavailable.
|
|
907
|
-
const targetList =
|
|
931
|
+
const defaultTargetList =
|
|
908
932
|
selectableLists.find((list) => list.id === board.default_list_id) ??
|
|
909
933
|
selectableLists[0];
|
|
934
|
+
const hoveredTargetList =
|
|
935
|
+
quickCreateTargetList === 'hovered_list'
|
|
936
|
+
? selectableLists.find((list) => list.id === hoveredTaskListId)
|
|
937
|
+
: undefined;
|
|
938
|
+
const targetList = hoveredTargetList ?? defaultTargetList;
|
|
910
939
|
if (!targetList) return;
|
|
911
940
|
createTask(board.id, targetList.id, selectableLists, filters);
|
|
912
941
|
},
|
|
@@ -1022,6 +1051,7 @@ export function BoardViews({
|
|
|
1022
1051
|
onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
|
|
1023
1052
|
canUseBoardAssignees={boardAssigneesEnabled}
|
|
1024
1053
|
assigneeMemberSource={assigneeMemberSource}
|
|
1054
|
+
onHoveredTaskListChange={setHoveredTaskListId}
|
|
1025
1055
|
readOnly={readOnly}
|
|
1026
1056
|
/>
|
|
1027
1057
|
);
|
|
@@ -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
|
-
? '
|
|
531
|
+
? 'board'
|
|
532
532
|
: effectiveAssigneeMemberSource,
|
|
533
533
|
})
|
|
534
534
|
);
|
package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx
CHANGED
|
@@ -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(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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(
|
|
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(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
89
|
-
|
|
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(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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(
|
|
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);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const DEFAULT_TASK_QUICK_CREATE_TARGET_LIST = 'default_list';
|
|
2
|
+
|
|
3
|
+
export const TASK_QUICK_CREATE_TARGET_LIST_VALUES = [
|
|
4
|
+
DEFAULT_TASK_QUICK_CREATE_TARGET_LIST,
|
|
5
|
+
'hovered_list',
|
|
6
|
+
] as const;
|
|
7
|
+
|
|
8
|
+
export type TaskQuickCreateTargetList =
|
|
9
|
+
(typeof TASK_QUICK_CREATE_TARGET_LIST_VALUES)[number];
|
|
10
|
+
|
|
11
|
+
export function normalizeTaskQuickCreateTargetList(
|
|
12
|
+
value: string | null | undefined,
|
|
13
|
+
fallback: TaskQuickCreateTargetList = DEFAULT_TASK_QUICK_CREATE_TARGET_LIST
|
|
14
|
+
): TaskQuickCreateTargetList {
|
|
15
|
+
return TASK_QUICK_CREATE_TARGET_LIST_VALUES.includes(
|
|
16
|
+
value as TaskQuickCreateTargetList
|
|
17
|
+
)
|
|
18
|
+
? (value as TaskQuickCreateTargetList)
|
|
19
|
+
: fallback;
|
|
20
|
+
}
|
|
@@ -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) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
3
|
+
import { getTaskApiUrl } from '../lib/tasks-app-url';
|
|
3
4
|
|
|
4
5
|
async function boardAction(
|
|
5
6
|
wsId: string,
|
|
@@ -7,13 +8,17 @@ async function boardAction(
|
|
|
7
8
|
method: 'PUT' | 'DELETE' | 'PATCH',
|
|
8
9
|
body?: any
|
|
9
10
|
) {
|
|
10
|
-
const response = await fetch(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const response = await fetch(
|
|
12
|
+
getTaskApiUrl(`/api/v1/workspaces/${wsId}/boards/${boardId}`),
|
|
13
|
+
{
|
|
14
|
+
method,
|
|
15
|
+
credentials: 'include',
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
},
|
|
19
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
20
|
+
}
|
|
21
|
+
);
|
|
17
22
|
|
|
18
23
|
if (!response.ok) {
|
|
19
24
|
const errorData = await response.json();
|
|
@@ -29,9 +34,10 @@ async function archiveAction(
|
|
|
29
34
|
method: 'POST' | 'DELETE'
|
|
30
35
|
) {
|
|
31
36
|
const response = await fetch(
|
|
32
|
-
`/api/v1/workspaces/${wsId}/boards/${boardId}/archive
|
|
37
|
+
getTaskApiUrl(`/api/v1/workspaces/${wsId}/boards/${boardId}/archive`),
|
|
33
38
|
{
|
|
34
39
|
method,
|
|
40
|
+
credentials: 'include',
|
|
35
41
|
headers: {
|
|
36
42
|
'Content-Type': 'application/json',
|
|
37
43
|
},
|
|
@@ -166,15 +172,19 @@ export function useBoardActions(wsId: string) {
|
|
|
166
172
|
{ boardId: string; options?: BoardActionOptions }
|
|
167
173
|
>({
|
|
168
174
|
mutationFn: ({ boardId }) =>
|
|
169
|
-
fetch(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
fetch(
|
|
176
|
+
getTaskApiUrl(`/api/v1/workspaces/${wsId}/task-boards/${boardId}/copy`),
|
|
177
|
+
{
|
|
178
|
+
method: 'POST',
|
|
179
|
+
credentials: 'include',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
},
|
|
183
|
+
body: JSON.stringify({
|
|
184
|
+
targetWorkspaceId: wsId,
|
|
185
|
+
}),
|
|
186
|
+
}
|
|
187
|
+
).then(async (res) => {
|
|
178
188
|
if (!res.ok) {
|
|
179
189
|
const errorData = await res.json();
|
|
180
190
|
throw new Error(errorData.error || 'Failed to duplicate board');
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
useRef,
|
|
30
30
|
useState,
|
|
31
31
|
} from 'react';
|
|
32
|
+
import { getTaskApiUrl } from '../lib/tasks-app-url';
|
|
32
33
|
import { useCalendarSync } from './use-calendar-sync';
|
|
33
34
|
|
|
34
35
|
// Utility function to round time to nearest 15-minute interval
|
|
@@ -319,11 +320,14 @@ async function syncTaskDurationAfterEventChange(
|
|
|
319
320
|
let totalScheduledMinutes = resizedEventMinutes;
|
|
320
321
|
|
|
321
322
|
const scheduleResponse = await fetch(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
getTaskApiUrl(
|
|
324
|
+
options?.isPersonalCalendar
|
|
325
|
+
? `/api/v1/users/me/tasks/${taskId}/schedule`
|
|
326
|
+
: `/api/v1/workspaces/${calendarWsId}/tasks/${taskId}/schedule`
|
|
327
|
+
),
|
|
325
328
|
{
|
|
326
329
|
cache: 'no-store',
|
|
330
|
+
credentials: 'include',
|
|
327
331
|
}
|
|
328
332
|
);
|
|
329
333
|
|