@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.
- package/CHANGELOG.md +71 -0
- package/package.json +82 -70
- package/src/components/ui/__tests__/avatar.test.tsx +8 -5
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
- package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
- package/src/components/ui/chart.test.tsx +29 -0
- package/src/components/ui/chart.tsx +12 -3
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +43 -13
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
- package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -3
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
- package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
- package/src/components/ui/custom/common-footer.tsx +16 -1
- package/src/components/ui/custom/production-indicator.tsx +1 -1
- package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
- package/src/components/ui/custom/settings/task-settings.tsx +18 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
- package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
- package/src/components/ui/custom/sidebar-context.tsx +61 -61
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
- package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
- package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
- package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
- package/src/components/ui/custom/workspace-select.tsx +33 -12
- package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
- package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
- package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
- package/src/components/ui/finance/invoices/hooks.ts +75 -20
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
- package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
- package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
- package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
- package/src/components/ui/finance/invoices/utils.test.ts +50 -0
- package/src/components/ui/finance/invoices/utils.ts +75 -17
- package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-types.ts +2 -0
- package/src/components/ui/finance/transactions/form.test.tsx +43 -0
- package/src/components/ui/finance/transactions/form.tsx +60 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
- package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
- package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
- package/src/components/ui/legacy/meet/page.test.ts +180 -0
- package/src/components/ui/legacy/meet/page.tsx +87 -39
- package/src/components/ui/legacy/meet/planId/page.tsx +10 -4
- package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
- package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
- package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
- package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
- package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
- package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
- package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
- package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
- package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
- package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-realtime-sync.test.tsx +37 -9
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +89 -70
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
- package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
- package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
- package/src/hooks/use-calendar-sync.tsx +22 -277
- package/src/hooks/use-calendar.tsx +95 -525
- package/src/hooks/use-task-actions.ts +43 -117
- package/src/hooks/use-user-config.ts +1 -1
- package/src/hooks/use-workspace-config.ts +6 -2
- package/src/hooks/use-workspace-presence.ts +1 -1
- 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
|
|
478
|
-
|
|
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
|
-
|
|
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 &&
|
|
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('
|
|
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: '
|
|
105
|
-
priority: '
|
|
106
|
-
start_date:
|
|
107
|
-
end_date:
|
|
108
|
-
estimation_points:
|
|
109
|
-
list_id: 'list-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
});
|