@tuturuuu/ui 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +5 -5
  3. package/src/components/ui/calendar-app/components/habits-panel.tsx +5 -2
  4. package/src/components/ui/calendar-app/components/priority-view.tsx +15 -7
  5. package/src/components/ui/calendar-app/components/scheduling-dialog.tsx +7 -3
  6. package/src/components/ui/calendar-app/components/task-scheduler-panel.tsx +7 -3
  7. package/src/components/ui/calendar-app/components/time-tracker/index.tsx +14 -9
  8. package/src/components/ui/custom/settings/task-settings.tsx +6 -2
  9. package/src/components/ui/custom/tables/data-table.tsx +58 -8
  10. package/src/components/ui/storefront/bundle-selection-dialog.tsx +249 -0
  11. package/src/components/ui/storefront/cart-summary-parts.tsx +28 -8
  12. package/src/components/ui/storefront/listing-card.tsx +6 -3
  13. package/src/components/ui/storefront/storefront-surface.test.tsx +134 -1
  14. package/src/components/ui/storefront/storefront-surface.tsx +66 -7
  15. package/src/components/ui/storefront/types.ts +15 -0
  16. package/src/components/ui/storefront/utils.ts +164 -2
  17. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -1
  18. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +3 -6
  19. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +11 -1
  20. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -3
  21. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-resource-context.test.ts +113 -0
  22. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-resource-context.ts +50 -0
  23. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +65 -18
  24. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +1 -1
  25. package/src/components/ui/tu-do/cycles/task-cycles-client.tsx +23 -14
  26. package/src/components/ui/tu-do/estimates/use-task-estimates.ts +9 -4
  27. package/src/components/ui/tu-do/habits/client.tsx +4 -1
  28. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +6 -3
  29. package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +19 -11
  30. package/src/components/ui/tu-do/my-tasks/use-my-tasks-query.ts +15 -6
  31. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +18 -13
  32. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +17 -10
  33. package/src/components/ui/tu-do/shared/fade-setting-initializer.tsx +3 -1
  34. package/src/components/ui/tu-do/shared/list-view.tsx +1 -1
  35. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +6 -2
  36. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-update-shared-task.test.ts +9 -5
  37. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +13 -8
  38. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-overrides.ts +21 -9
  39. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-update-shared-task.ts +10 -5
  40. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +4 -2
  41. package/src/components/ui/tu-do/shared/task-estimation-picker.tsx +5 -2
  42. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +1 -1
  43. package/src/hooks/__tests__/use-task-actions.test.tsx +51 -0
  44. package/src/hooks/task-actions-personal-external.ts +2 -3
  45. package/src/hooks/use-board-actions.ts +27 -17
  46. package/src/hooks/use-calendar.tsx +7 -3
  47. package/src/hooks/use-settings-dialog-shortcut.ts +51 -0
  48. package/src/lib/task-personal-external.ts +49 -0
  49. package/src/lib/tasks-app-url.ts +85 -0
@@ -1,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(`/api/v1/workspaces/${wsId}/boards/${boardId}`, {
11
- method,
12
- headers: {
13
- 'Content-Type': 'application/json',
14
- },
15
- body: body ? JSON.stringify(body) : undefined,
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(`/api/v1/workspaces/${wsId}/task-boards/${boardId}/copy`, {
170
- method: 'POST',
171
- headers: {
172
- 'Content-Type': 'application/json',
173
- },
174
- body: JSON.stringify({
175
- targetWorkspaceId: wsId,
176
- }),
177
- }).then(async (res) => {
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
- options?.isPersonalCalendar
323
- ? `/api/v1/users/me/tasks/${taskId}/schedule`
324
- : `/api/v1/workspaces/${calendarWsId}/tasks/${taskId}/schedule`,
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
+ }