@tuturuuu/ui 0.12.0 → 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 +16 -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 +6 -2
- package/src/components/ui/custom/tables/data-table.tsx +58 -8
- 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
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);
|
|
@@ -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
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
interface UseSettingsDialogShortcutOptions {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
onOpen: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isEditableShortcutTarget(target: EventTarget | null) {
|
|
11
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
12
|
+
|
|
13
|
+
const tagName = target.tagName.toLowerCase();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
target.isContentEditable ||
|
|
17
|
+
tagName === 'input' ||
|
|
18
|
+
tagName === 'textarea' ||
|
|
19
|
+
tagName === 'select'
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isSettingsDialogShortcut(event: KeyboardEvent) {
|
|
24
|
+
return (event.metaKey || event.ctrlKey) && !event.altKey && event.key === ',';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Opens the app settings dialog on Cmd/Ctrl+, — the platform-wide convention.
|
|
29
|
+
* Ignores the shortcut while typing in editable fields and when another handler
|
|
30
|
+
* has already called preventDefault(). Shared by apps/web and every satellite
|
|
31
|
+
* app (wired once in the satellite user-nav shell).
|
|
32
|
+
*/
|
|
33
|
+
export function useSettingsDialogShortcut({
|
|
34
|
+
enabled,
|
|
35
|
+
onOpen,
|
|
36
|
+
}: UseSettingsDialogShortcutOptions) {
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!enabled) return;
|
|
39
|
+
|
|
40
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
41
|
+
if (event.defaultPrevented || !isSettingsDialogShortcut(event)) return;
|
|
42
|
+
if (isEditableShortcutTarget(event.target)) return;
|
|
43
|
+
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
onOpen();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
49
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
50
|
+
}, [enabled, onOpen]);
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
|
+
import { isPersonalExternalStagingListId } from '@tuturuuu/utils/task-helper';
|
|
3
|
+
|
|
4
|
+
function hasValue(value: string | null | undefined): value is string {
|
|
5
|
+
return typeof value === 'string' && value.length > 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isPersonalExternalOverlayTask(task?: Task | null) {
|
|
9
|
+
if (!task) return false;
|
|
10
|
+
|
|
11
|
+
if (task.is_personal_external === true) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (task.is_personal_external === false) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (isPersonalExternalStagingListId(task.list_id)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const personalBoardId = hasValue(task.personal_board_id)
|
|
24
|
+
? task.personal_board_id
|
|
25
|
+
: null;
|
|
26
|
+
const personalListId = hasValue(task.personal_list_id)
|
|
27
|
+
? task.personal_list_id
|
|
28
|
+
: null;
|
|
29
|
+
|
|
30
|
+
if (!personalBoardId && !personalListId) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
hasValue(task.source_board_id) &&
|
|
36
|
+
personalBoardId &&
|
|
37
|
+
task.source_board_id !== personalBoardId
|
|
38
|
+
) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
!hasValue(task.source_board_id) &&
|
|
44
|
+
hasValue(task.source_workspace_id) &&
|
|
45
|
+
hasValue(task.source_list_id) &&
|
|
46
|
+
personalListId !== null &&
|
|
47
|
+
task.source_list_id !== personalListId
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
function normalizeOrigin(value?: string) {
|
|
2
|
+
if (!value) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const [firstValue] = value
|
|
7
|
+
.split(/[,\n]/u)
|
|
8
|
+
.map((entry) => entry.trim())
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
|
|
11
|
+
if (!firstValue) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const normalized = /^[a-z]+:\/\//iu.test(firstValue)
|
|
16
|
+
? firstValue
|
|
17
|
+
: `https://${firstValue}`;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return new URL(normalized).origin;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getTasksAppOriginForBrowser() {
|
|
27
|
+
const configured =
|
|
28
|
+
normalizeOrigin(process.env.NEXT_PUBLIC_TASKS_APP_URL) ??
|
|
29
|
+
normalizeOrigin(process.env.NEXT_PUBLIC_TUDO_APP_URL);
|
|
30
|
+
|
|
31
|
+
if (configured) {
|
|
32
|
+
return configured;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof window === 'undefined') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { hostname, port, protocol } = window.location;
|
|
40
|
+
const normalizedHostname = hostname.toLowerCase();
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
normalizedHostname === 'tasks.tuturuuu.com' ||
|
|
44
|
+
normalizedHostname === 'tasks.tuturuuu.localhost' ||
|
|
45
|
+
port === '7809'
|
|
46
|
+
) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
normalizedHostname === 'localhost' ||
|
|
52
|
+
normalizedHostname === '127.0.0.1' ||
|
|
53
|
+
normalizedHostname === '::1' ||
|
|
54
|
+
normalizedHostname === '[::1]'
|
|
55
|
+
) {
|
|
56
|
+
return port ? `${protocol}//${hostname}:7809` : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
normalizedHostname === 'tuturuuu.localhost' ||
|
|
61
|
+
normalizedHostname.endsWith('.tuturuuu.localhost')
|
|
62
|
+
) {
|
|
63
|
+
return `${protocol}//tasks.tuturuuu.localhost`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
normalizedHostname === 'tuturuuu.com' ||
|
|
68
|
+
normalizedHostname.endsWith('.tuturuuu.com')
|
|
69
|
+
) {
|
|
70
|
+
return 'https://tasks.tuturuuu.com';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getTasksAppUrl(path: string) {
|
|
77
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
78
|
+
const origin = getTasksAppOriginForBrowser();
|
|
79
|
+
|
|
80
|
+
return origin ? new URL(normalizedPath, origin).toString() : normalizedPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getTaskApiUrl(path: string) {
|
|
84
|
+
return getTasksAppUrl(path);
|
|
85
|
+
}
|