@tuturuuu/ui 0.1.0 → 0.3.1

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 (128) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +82 -70
  3. package/src/components/ui/__tests__/avatar.test.tsx +8 -5
  4. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
  5. package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
  6. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
  8. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
  9. package/src/components/ui/chart.test.tsx +29 -0
  10. package/src/components/ui/chart.tsx +12 -3
  11. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +43 -13
  12. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
  13. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
  14. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
  15. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
  16. package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
  17. package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -3
  18. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  19. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  20. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  21. package/src/components/ui/custom/common-footer.tsx +16 -1
  22. package/src/components/ui/custom/production-indicator.tsx +1 -1
  23. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  24. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  25. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  26. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  27. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  28. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  29. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  30. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  31. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  32. package/src/components/ui/custom/workspace-select.tsx +33 -12
  33. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  34. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  35. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  36. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  37. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  38. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  39. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  40. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  41. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  42. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  43. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  44. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  45. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  46. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  47. package/src/components/ui/finance/invoices/utils.ts +75 -17
  48. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  49. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  50. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  51. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  52. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  53. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  54. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  55. package/src/components/ui/finance/transactions/form.tsx +60 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  57. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  58. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  59. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  60. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  61. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  62. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  63. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  64. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  65. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  66. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  67. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  68. package/src/components/ui/legacy/meet/page.tsx +87 -39
  69. package/src/components/ui/legacy/meet/planId/page.tsx +10 -4
  70. package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
  71. package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
  72. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  73. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  74. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  75. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  77. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  78. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  79. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  80. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  81. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  82. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  83. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  84. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  85. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  86. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  87. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  88. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  89. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  90. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  91. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  92. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  93. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  94. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  95. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  96. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  97. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  98. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  99. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  100. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  101. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  102. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  103. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  104. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  105. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  106. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  107. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  108. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  109. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  110. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  111. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  112. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  113. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-realtime-sync.test.tsx +37 -9
  114. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  115. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +89 -70
  116. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
  117. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
  118. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
  119. package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
  120. package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
  121. package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
  122. package/src/hooks/use-calendar-sync.tsx +22 -277
  123. package/src/hooks/use-calendar.tsx +95 -525
  124. package/src/hooks/use-task-actions.ts +43 -117
  125. package/src/hooks/use-user-config.ts +1 -1
  126. package/src/hooks/use-workspace-config.ts +6 -2
  127. package/src/hooks/use-workspace-presence.ts +1 -1
  128. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -42,6 +42,7 @@ import {
42
42
  DropdownMenuSubTrigger,
43
43
  DropdownMenuTrigger,
44
44
  } from '@tuturuuu/ui/dropdown-menu';
45
+ import { useUserBooleanConfig } from '@tuturuuu/ui/hooks/use-user-config';
45
46
  import { Separator } from '@tuturuuu/ui/separator';
46
47
  import {
47
48
  Table,
@@ -53,7 +54,6 @@ import {
53
54
  } from '@tuturuuu/ui/table';
54
55
  import { TooltipProvider } from '@tuturuuu/ui/tooltip';
55
56
  import { cn } from '@tuturuuu/utils/format';
56
- import { isTaskBoardResolvedStatus } from '@tuturuuu/utils/task-list-status';
57
57
  import { format, isPast, isToday, isTomorrow } from 'date-fns';
58
58
  import { enUS, vi } from 'date-fns/locale';
59
59
  import Image from 'next/image';
@@ -69,6 +69,10 @@ import {
69
69
  type ListViewSortOrder,
70
70
  sortListViewTasks,
71
71
  } from './list-view-sorting';
72
+ import {
73
+ shouldShowTaskDueDate,
74
+ TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID,
75
+ } from './task-due-date-visibility';
72
76
  import { TaskRowActionsMenu } from './task-row-actions-menu';
73
77
 
74
78
  interface Props {
@@ -114,6 +118,10 @@ export function ListView({
114
118
  const queryClient = useQueryClient();
115
119
  const { resolvedTheme } = useTheme();
116
120
  const isDark = resolvedTheme === 'dark';
121
+ const { value: showReviewDueDates } = useUserBooleanConfig(
122
+ TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID,
123
+ false
124
+ );
117
125
  const [localTasks, setLocalTasks] = useState<Task[]>(tasks);
118
126
  const [sortField, setSortField] = useState<ListViewSortField>('created_at');
119
127
  const [sortOrder, setSortOrder] = useState<ListViewSortOrder>('desc');
@@ -474,15 +482,17 @@ export function ListView({
474
482
  <TableBody>
475
483
  {displayedTasks.map((task) => {
476
484
  const taskList = getTaskList(task);
477
- const taskIsInResolvedList = isTaskBoardResolvedStatus(
478
- taskList?.status
479
- );
485
+ const taskDueDateVisible = shouldShowTaskDueDate({
486
+ completedAt: task.completed_at,
487
+ closedAt: task.closed_at,
488
+ dueDate: task.end_date,
489
+ listStatus: taskList?.status,
490
+ showReviewDueDates,
491
+ });
480
492
  const taskIsPastDue = Boolean(
481
493
  task.end_date &&
482
494
  isPast(new Date(task.end_date)) &&
483
- !task.closed_at &&
484
- !task.completed_at &&
485
- !taskIsInResolvedList
495
+ taskDueDateVisible
486
496
  );
487
497
  const isExternalSource = Boolean(
488
498
  task.source_board_id && task.source_board_id !== boardId
@@ -656,7 +666,7 @@ export function ListView({
656
666
  )}
657
667
  {columnVisibility.end_date && (
658
668
  <TableCell className="px-2 py-0">
659
- {task.end_date && !taskIsInResolvedList && (
669
+ {taskDueDateVisible && task.end_date && (
660
670
  <div className="flex items-center gap-1.5">
661
671
  <span
662
672
  className={cn(
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ shouldShowTaskDueDate,
4
+ shouldShowTaskStartDate,
5
+ } from './task-due-date-visibility';
6
+
7
+ describe('task due date visibility', () => {
8
+ it('shows due dates for active workflow tasks', () => {
9
+ expect(
10
+ shouldShowTaskDueDate({
11
+ dueDate: '2026-06-08T00:00:00.000Z',
12
+ listStatus: 'active',
13
+ showReviewDueDates: false,
14
+ })
15
+ ).toBe(true);
16
+ });
17
+
18
+ it('hides review due dates by default', () => {
19
+ expect(
20
+ shouldShowTaskDueDate({
21
+ dueDate: '2026-06-08T00:00:00.000Z',
22
+ listStatus: 'review',
23
+ showReviewDueDates: false,
24
+ })
25
+ ).toBe(false);
26
+ });
27
+
28
+ it('shows review due dates when the setting is enabled', () => {
29
+ expect(
30
+ shouldShowTaskDueDate({
31
+ dueDate: '2026-06-08T00:00:00.000Z',
32
+ listStatus: 'review',
33
+ showReviewDueDates: true,
34
+ })
35
+ ).toBe(true);
36
+ });
37
+
38
+ it('keeps terminal and completed tasks hidden', () => {
39
+ expect(
40
+ shouldShowTaskDueDate({
41
+ dueDate: '2026-06-08T00:00:00.000Z',
42
+ listStatus: 'done',
43
+ showReviewDueDates: true,
44
+ })
45
+ ).toBe(false);
46
+ expect(
47
+ shouldShowTaskDueDate({
48
+ closedAt: '2026-06-08T00:00:00.000Z',
49
+ dueDate: '2026-06-08T00:00:00.000Z',
50
+ listStatus: 'active',
51
+ showReviewDueDates: true,
52
+ })
53
+ ).toBe(false);
54
+ expect(
55
+ shouldShowTaskDueDate({
56
+ completedAt: '2026-06-08T00:00:00.000Z',
57
+ dueDate: '2026-06-08T00:00:00.000Z',
58
+ listStatus: 'review',
59
+ showReviewDueDates: true,
60
+ })
61
+ ).toBe(false);
62
+ });
63
+
64
+ it('does not use the review due-date setting for start dates', () => {
65
+ expect(
66
+ shouldShowTaskStartDate({
67
+ listStatus: 'review',
68
+ startDate: '2026-06-08T00:00:00.000Z',
69
+ })
70
+ ).toBe(false);
71
+ });
72
+ });
@@ -0,0 +1,38 @@
1
+ import type { TaskBoardStatus } from '@tuturuuu/types/primitives/TaskBoard';
2
+ import { isTaskBoardTerminalStatus } from '@tuturuuu/utils/task-list-status';
3
+
4
+ export const TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID =
5
+ 'TASKS_SHOW_REVIEW_DUE_DATES';
6
+
7
+ export function shouldShowTaskDueDate({
8
+ completedAt,
9
+ closedAt,
10
+ dueDate,
11
+ listStatus,
12
+ showReviewDueDates,
13
+ }: {
14
+ completedAt?: string | null;
15
+ closedAt?: string | null;
16
+ dueDate?: string | null;
17
+ listStatus?: TaskBoardStatus | string | null;
18
+ showReviewDueDates: boolean;
19
+ }) {
20
+ if (!dueDate || completedAt || closedAt) return false;
21
+ if (listStatus === 'review') return showReviewDueDates;
22
+ return !isTaskBoardTerminalStatus(listStatus);
23
+ }
24
+
25
+ export function shouldShowTaskStartDate({
26
+ completedAt,
27
+ closedAt,
28
+ listStatus,
29
+ startDate,
30
+ }: {
31
+ completedAt?: string | null;
32
+ closedAt?: string | null;
33
+ listStatus?: TaskBoardStatus | string | null;
34
+ startDate?: string | null;
35
+ }) {
36
+ if (!startDate || completedAt || closedAt) return false;
37
+ return listStatus !== 'review' && !isTaskBoardTerminalStatus(listStatus);
38
+ }
@@ -90,29 +90,48 @@ describe('useTaskRealtimeSync', () => {
90
90
  );
91
91
  });
92
92
 
93
- it('applies a matching task upsert to the open dialog state', () => {
93
+ it('refetches a matching task upsert before applying open dialog state', async () => {
94
94
  const start = '2026-05-22T01:00:00.000Z';
95
95
  const end = '2026-05-22T02:00:00.000Z';
96
96
  const props = makeProps();
97
97
 
98
+ taskApiMocks.getWorkspaceTask.mockResolvedValueOnce({
99
+ task: {
100
+ id: 'task-1',
101
+ name: 'Server task',
102
+ priority: 'high',
103
+ start_date: start,
104
+ end_date: end,
105
+ estimation_points: 5,
106
+ list_id: 'list-2',
107
+ },
108
+ });
109
+
98
110
  renderHook(() => useTaskRealtimeSync(props), { wrapper });
99
111
 
100
112
  act(() => {
101
113
  boardRealtimeMocks.onTaskChange?.(
102
114
  {
103
115
  id: 'task-1',
104
- name: 'Remote task',
105
- priority: 'high',
106
- start_date: start,
107
- end_date: end,
108
- estimation_points: 5,
109
- list_id: 'list-2',
116
+ name: 'Untrusted broadcast task',
117
+ priority: 'critical',
118
+ start_date: '2026-05-23T01:00:00.000Z',
119
+ end_date: '2026-05-23T02:00:00.000Z',
120
+ estimation_points: 8,
121
+ list_id: 'list-3',
110
122
  } as Task,
111
123
  'UPDATE'
112
124
  );
113
125
  });
114
126
 
115
- expect(props.setName).toHaveBeenCalledWith('Remote task');
127
+ await waitFor(() => {
128
+ expect(props.setName).toHaveBeenCalledWith('Server task');
129
+ });
130
+ expect(taskApiMocks.getWorkspaceTask).toHaveBeenCalledWith(
131
+ 'ws-1',
132
+ 'task-1'
133
+ );
134
+ expect(props.setName).not.toHaveBeenCalledWith('Untrusted broadcast task');
116
135
  expect(props.setPriority).toHaveBeenCalledWith('high');
117
136
  expect(props.setStartDate).toHaveBeenCalledWith(new Date(start));
118
137
  expect(props.setEndDate).toHaveBeenCalledWith(new Date(end));
@@ -137,10 +156,13 @@ describe('useTaskRealtimeSync', () => {
137
156
  expect(props.setSelectedListId).not.toHaveBeenCalled();
138
157
  });
139
158
 
140
- it('does not overwrite a local pending title edit', () => {
159
+ it('does not overwrite a local pending title edit', async () => {
141
160
  const props = makeProps({
142
161
  pendingNameRef: { current: 'Local draft title' },
143
162
  });
163
+ taskApiMocks.getWorkspaceTask.mockResolvedValueOnce({
164
+ task: { id: 'task-1', name: 'Remote title' },
165
+ });
144
166
 
145
167
  renderHook(() => useTaskRealtimeSync(props), { wrapper });
146
168
 
@@ -151,6 +173,12 @@ describe('useTaskRealtimeSync', () => {
151
173
  );
152
174
  });
153
175
 
176
+ await waitFor(() => {
177
+ expect(taskApiMocks.getWorkspaceTask).toHaveBeenCalledWith(
178
+ 'ws-1',
179
+ 'task-1'
180
+ );
181
+ });
154
182
  expect(props.setName).not.toHaveBeenCalled();
155
183
  });
156
184
 
@@ -1,7 +1,7 @@
1
1
  import { useQueryClient } from '@tanstack/react-query';
2
2
  import {
3
3
  type TaskSchedulingUpdatePayload,
4
- updateTaskSchedulingSettings,
4
+ updateCurrentUserTaskSchedulingSettings,
5
5
  } from '@tuturuuu/internal-api';
6
6
  import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
7
7
  import type { CalendarHoursType } from '@tuturuuu/types/primitives/Task';
@@ -482,7 +482,10 @@ export function useTaskMutations({
482
482
  auto_schedule: settings.autoSchedule,
483
483
  };
484
484
 
485
- await updateTaskSchedulingSettings(wsId, taskId, schedulingPayload);
485
+ await updateCurrentUserTaskSchedulingSettings(
486
+ taskId,
487
+ schedulingPayload
488
+ );
486
489
 
487
490
  // Keep any related query data consistent
488
491
  queryClient.invalidateQueries({
@@ -519,7 +522,7 @@ export function useTaskMutations({
519
522
  setSchedulingSaving(false);
520
523
  }
521
524
  },
522
- [isCreateMode, taskId, queryClient, toast, triggerRefresh, wsId]
525
+ [isCreateMode, taskId, queryClient, toast, triggerRefresh]
523
526
  );
524
527
 
525
528
  return {
@@ -93,6 +93,7 @@ export function useTaskRealtimeSync({
93
93
  const endDateRef = useRef(endDate);
94
94
  const estimationPointsRef = useRef(estimationPoints);
95
95
  const selectedListIdRef = useRef(selectedListId);
96
+ const taskRequestTokenRef = useRef(0);
96
97
  const relationsRequestTokenRef = useRef(0);
97
98
 
98
99
  nameRef.current = name;
@@ -105,7 +106,7 @@ export function useTaskRealtimeSync({
105
106
  const realtimeActive =
106
107
  !isCreateMode && isOpen && realtimeEnabled && !!taskId && !disabled;
107
108
 
108
- const fetchTaskRelations = useCallback(async () => {
109
+ const fetchLatestTask = useCallback(async () => {
109
110
  if (!taskId) return null;
110
111
 
111
112
  try {
@@ -127,25 +128,96 @@ export function useTaskRealtimeSync({
127
128
  staleTime: 0,
128
129
  });
129
130
 
130
- const relationshipProjectIds =
131
- routeTask.task.project_ids ??
132
- routeTask.task.projects?.map((project) => project.id) ??
133
- [];
134
-
135
- const filteredProjects = (routeTask.task.projects ?? []).filter(
136
- (project) => relationshipProjectIds.includes(project.id)
137
- );
138
-
139
- return {
140
- labels: routeTask.task.labels ?? [],
141
- assignees: routeTask.task.assignees ?? [],
142
- projects: filteredProjects,
143
- };
131
+ return routeTask.task;
144
132
  } catch {
145
133
  return null;
146
134
  }
147
135
  }, [queryClient, taskId, taskWorkspaceId, wsId]);
148
136
 
137
+ const applyLatestTaskFields = useCallback(async () => {
138
+ if (!realtimeActive || !taskId) return;
139
+
140
+ taskRequestTokenRef.current += 1;
141
+ const token = taskRequestTokenRef.current;
142
+ const task = await fetchLatestTask();
143
+
144
+ if (token !== taskRequestTokenRef.current || !task || task.id !== taskId) {
145
+ return;
146
+ }
147
+
148
+ if (
149
+ hasOwn(task, 'name') &&
150
+ typeof task.name === 'string' &&
151
+ !pendingNameRef.current &&
152
+ task.name !== nameRef.current
153
+ ) {
154
+ setName(task.name);
155
+ }
156
+
157
+ if (hasOwn(task, 'priority') && task.priority !== priorityRef.current) {
158
+ setPriority(task.priority ?? null);
159
+ }
160
+
161
+ if (hasOwn(task, 'start_date')) {
162
+ const nextStartDate = toDate(task.start_date);
163
+ if (!datesMatch(startDateRef.current, nextStartDate)) {
164
+ setStartDate(nextStartDate);
165
+ }
166
+ }
167
+
168
+ if (hasOwn(task, 'end_date')) {
169
+ const nextEndDate = toDate(task.end_date);
170
+ if (!datesMatch(endDateRef.current, nextEndDate)) {
171
+ setEndDate(nextEndDate);
172
+ }
173
+ }
174
+
175
+ if (
176
+ hasOwn(task, 'estimation_points') &&
177
+ task.estimation_points !== estimationPointsRef.current
178
+ ) {
179
+ setEstimationPoints(task.estimation_points ?? null);
180
+ }
181
+
182
+ if (
183
+ hasOwn(task, 'list_id') &&
184
+ typeof task.list_id === 'string' &&
185
+ task.list_id !== selectedListIdRef.current
186
+ ) {
187
+ setSelectedListId(task.list_id);
188
+ }
189
+ }, [
190
+ fetchLatestTask,
191
+ pendingNameRef,
192
+ realtimeActive,
193
+ setEndDate,
194
+ setEstimationPoints,
195
+ setName,
196
+ setPriority,
197
+ setSelectedListId,
198
+ setStartDate,
199
+ taskId,
200
+ ]);
201
+
202
+ const fetchTaskRelations = useCallback(async () => {
203
+ const task = await fetchLatestTask();
204
+
205
+ if (!task) return null;
206
+
207
+ const relationshipProjectIds =
208
+ task.project_ids ?? task.projects?.map((project) => project.id) ?? [];
209
+
210
+ const filteredProjects = (task.projects ?? []).filter((project) =>
211
+ relationshipProjectIds.includes(project.id)
212
+ );
213
+
214
+ return {
215
+ labels: task.labels ?? [],
216
+ assignees: task.assignees ?? [],
217
+ projects: filteredProjects,
218
+ };
219
+ }, [fetchLatestTask]);
220
+
149
221
  const applyLatestRelations = useCallback(async () => {
150
222
  if (!realtimeActive) return;
151
223
 
@@ -175,62 +247,9 @@ export function useTaskRealtimeSync({
175
247
  )
176
248
  return;
177
249
 
178
- if (
179
- hasOwn(updatedTask, 'name') &&
180
- typeof updatedTask.name === 'string' &&
181
- !pendingNameRef.current &&
182
- updatedTask.name !== nameRef.current
183
- ) {
184
- setName(updatedTask.name);
185
- }
186
-
187
- if (
188
- hasOwn(updatedTask, 'priority') &&
189
- updatedTask.priority !== priorityRef.current
190
- ) {
191
- setPriority(updatedTask.priority ?? null);
192
- }
193
-
194
- if (hasOwn(updatedTask, 'start_date')) {
195
- const nextStartDate = toDate(updatedTask.start_date);
196
- if (!datesMatch(startDateRef.current, nextStartDate)) {
197
- setStartDate(nextStartDate);
198
- }
199
- }
200
-
201
- if (hasOwn(updatedTask, 'end_date')) {
202
- const nextEndDate = toDate(updatedTask.end_date);
203
- if (!datesMatch(endDateRef.current, nextEndDate)) {
204
- setEndDate(nextEndDate);
205
- }
206
- }
207
-
208
- if (
209
- hasOwn(updatedTask, 'estimation_points') &&
210
- updatedTask.estimation_points !== estimationPointsRef.current
211
- ) {
212
- setEstimationPoints(updatedTask.estimation_points ?? null);
213
- }
214
-
215
- if (
216
- hasOwn(updatedTask, 'list_id') &&
217
- typeof updatedTask.list_id === 'string' &&
218
- updatedTask.list_id !== selectedListIdRef.current
219
- ) {
220
- setSelectedListId(updatedTask.list_id);
221
- }
250
+ void applyLatestTaskFields();
222
251
  },
223
- [
224
- pendingNameRef,
225
- realtimeActive,
226
- setEndDate,
227
- setEstimationPoints,
228
- setName,
229
- setPriority,
230
- setSelectedListId,
231
- setStartDate,
232
- taskId,
233
- ]
252
+ [applyLatestTaskFields, realtimeActive, taskId]
234
253
  );
235
254
 
236
255
  const handleTaskRelationsChange = useCallback(
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { QueryClient } from '@tanstack/react-query';
4
4
  import type { Editor, JSONContent } from '@tiptap/react';
5
- import { updateTaskSchedulingSettings } from '@tuturuuu/internal-api';
5
+ import { updateCurrentUserTaskSchedulingSettings } from '@tuturuuu/internal-api';
6
6
  import type { WorkspaceTaskUpdatePayload } from '@tuturuuu/internal-api/tasks';
7
7
  import { createWorkspaceTaskRelationship } from '@tuturuuu/internal-api/tasks';
8
8
  import { createClient } from '@tuturuuu/supabase/next/client';
@@ -975,7 +975,7 @@ async function handleCreateTask({
975
975
 
976
976
  if (hasAnySchedulingValue) {
977
977
  try {
978
- await updateTaskSchedulingSettings(wsId, newTask.id, {
978
+ await updateCurrentUserTaskSchedulingSettings(newTask.id, {
979
979
  total_duration: totalDuration,
980
980
  is_splittable: isSplittable,
981
981
  min_split_duration_minutes: minSplitDurationMinutes,
@@ -38,6 +38,7 @@ import {
38
38
  TaskDueDateMenu,
39
39
  TaskMoveMenu,
40
40
  TaskPriorityMenu,
41
+ TaskSchedulingMenu,
41
42
  } from '../boards/boardId/menus';
42
43
  import { useTaskDialog } from '../hooks/useTaskDialog';
43
44
  import { useTasksHref } from '../tasks-route-context';
@@ -87,6 +88,7 @@ export function TaskRowActionsMenu({
87
88
  }: TaskRowActionsMenuProps) {
88
89
  const t = useTranslations('common');
89
90
  const tTasks = useTranslations('ws-tasks');
91
+ const taskBoardT = useTranslations();
90
92
  const tasksHref = useTasksHref();
91
93
  const { openTask } = useTaskDialog();
92
94
  const [internalOpen, setInternalOpen] = useState(false);
@@ -287,6 +289,37 @@ export function TaskRowActionsMenu({
287
289
  }}
288
290
  />
289
291
  )}
292
+ {taskList?.status !== 'documents' && (
293
+ <TaskSchedulingMenu
294
+ task={task}
295
+ boardId={boardId}
296
+ isLoading={isLoading}
297
+ onUpdate={onUpdate}
298
+ onClose={() => setMenuOpen(false)}
299
+ translations={{
300
+ schedule: taskBoardT('ws-task-boards.dialog.schedule'),
301
+ estimatedDuration: taskBoardT(
302
+ 'ws-task-boards.dialog.estimated_duration'
303
+ ),
304
+ h: taskBoardT('ws-task-boards.dialog.h'),
305
+ m: taskBoardT('ws-task-boards.dialog.m'),
306
+ splittable: taskBoardT('ws-task-boards.dialog.splittable'),
307
+ minSplit: taskBoardT('ws-task-boards.dialog.min_split'),
308
+ maxSplit: taskBoardT('ws-task-boards.dialog.max_split'),
309
+ hourType: taskBoardT('ws-task-boards.dialog.hour_type'),
310
+ workHours: taskBoardT('ws-task-boards.dialog.work_hours'),
311
+ meetingHours: taskBoardT('ws-task-boards.dialog.meeting_hours'),
312
+ personalHours: taskBoardT(
313
+ 'ws-task-boards.dialog.personal_hours'
314
+ ),
315
+ autoSchedule: taskBoardT('ws-task-boards.dialog.auto_schedule'),
316
+ save: t('save'),
317
+ clear: t('clear'),
318
+ saved: t('saved'),
319
+ error: t('error'),
320
+ }}
321
+ />
322
+ )}
290
323
  {canUseCurrentBoardLists && (
291
324
  <TaskMoveMenu
292
325
  currentListId={task.list_id}
@@ -1,17 +1,26 @@
1
- import { renderHook } from '@testing-library/react';
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
2
3
  import type { ReactNode } from 'react';
3
- import { describe, expect, it, vi } from 'vitest';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
5
  import { CalendarProvider, useCalendar } from '../use-calendar';
5
6
 
7
+ const calendarMockState = vi.hoisted(() => ({
8
+ events: [] as CalendarEvent[],
9
+ }));
10
+
6
11
  // Mock useCalendarSync
7
12
  vi.mock('../use-calendar-sync', () => ({
8
13
  useCalendarSync: () => ({
9
- events: [],
14
+ events: calendarMockState.events,
10
15
  refresh: vi.fn(),
11
16
  }),
12
17
  }));
13
18
 
14
19
  describe('CalendarProvider Read-Only Mode', () => {
20
+ beforeEach(() => {
21
+ calendarMockState.events = [];
22
+ });
23
+
15
24
  const mockUseQuery = vi
16
25
  .fn()
17
26
  .mockReturnValue({ data: null, isLoading: false });
@@ -87,4 +96,66 @@ describe('CalendarProvider Read-Only Mode', () => {
87
96
  // If it doesn't throw, it's fine (it just exits early with a warning)
88
97
  expect(true).toBe(true);
89
98
  });
99
+
100
+ it('opens preview for existing event clicks and editor from Edit action', () => {
101
+ calendarMockState.events = [
102
+ {
103
+ id: 'event-1',
104
+ title: 'Design review',
105
+ start_at: '2026-06-08T09:00:00.000Z',
106
+ end_at: '2026-06-08T10:00:00.000Z',
107
+ ws_id: 'workspace-1',
108
+ },
109
+ ];
110
+
111
+ const wrapper = ({ children }: { children: ReactNode }) => (
112
+ <CalendarProvider
113
+ ws={{ id: 'workspace-1', name: 'Workspace' } as any}
114
+ useQuery={mockUseQuery}
115
+ useQueryClient={mockUseQueryClient}
116
+ >
117
+ {children}
118
+ </CalendarProvider>
119
+ );
120
+
121
+ const { result } = renderHook(() => useCalendar(), { wrapper });
122
+
123
+ act(() => {
124
+ result.current.openModal('event-1');
125
+ });
126
+
127
+ expect(result.current.isPreviewOpen).toBe(true);
128
+ expect(result.current.previewEvent?.id).toBe('event-1');
129
+ expect(result.current.isModalOpen).toBe(false);
130
+
131
+ act(() => {
132
+ result.current.openEventEditor('event-1');
133
+ });
134
+
135
+ expect(result.current.isPreviewOpen).toBe(false);
136
+ expect(result.current.isModalOpen).toBe(true);
137
+ expect(result.current.activeEvent?.id).toBe('event-1');
138
+ });
139
+
140
+ it('opens the editor directly for create flows', () => {
141
+ const wrapper = ({ children }: { children: ReactNode }) => (
142
+ <CalendarProvider
143
+ ws={{ id: 'workspace-1', name: 'Workspace' } as any}
144
+ useQuery={mockUseQuery}
145
+ useQueryClient={mockUseQueryClient}
146
+ >
147
+ {children}
148
+ </CalendarProvider>
149
+ );
150
+
151
+ const { result } = renderHook(() => useCalendar(), { wrapper });
152
+
153
+ act(() => {
154
+ result.current.openModal(undefined, 'event');
155
+ });
156
+
157
+ expect(result.current.isPreviewOpen).toBe(false);
158
+ expect(result.current.isModalOpen).toBe(true);
159
+ expect(result.current.activeEvent?.id).toBe('new');
160
+ });
90
161
  });