@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,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
|
+
}
|