@tuturuuu/ui 0.2.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 (116) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/package.json +79 -67
  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/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  12. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  13. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  14. package/src/components/ui/custom/common-footer.tsx +16 -1
  15. package/src/components/ui/custom/production-indicator.tsx +1 -1
  16. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  17. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  18. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  19. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  20. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  21. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  22. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  23. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  24. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  25. package/src/components/ui/custom/workspace-select.tsx +33 -12
  26. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  27. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  28. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  29. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  30. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  31. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  32. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  33. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  34. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  35. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  36. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  37. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  38. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  39. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  40. package/src/components/ui/finance/invoices/utils.ts +75 -17
  41. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  42. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  43. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  44. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  45. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  46. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  47. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  48. package/src/components/ui/finance/transactions/form.tsx +60 -0
  49. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  50. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  51. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  52. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  53. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  54. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  55. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  56. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  57. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  58. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  59. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  60. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  61. package/src/components/ui/legacy/meet/page.tsx +87 -39
  62. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  63. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  64. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  65. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  66. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  67. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  68. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  69. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  70. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  71. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  72. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  73. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  74. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  75. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  77. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  78. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  79. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  80. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  81. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  82. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  83. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  84. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  85. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  86. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  87. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  88. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  89. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  90. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  91. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  92. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  93. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  94. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  95. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  96. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  97. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  98. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  99. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  100. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  101. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  102. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  103. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  104. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
  105. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
  106. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
  107. package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
  108. package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
  109. package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
  110. package/src/hooks/use-calendar-sync.tsx +22 -277
  111. package/src/hooks/use-calendar.tsx +95 -525
  112. package/src/hooks/use-task-actions.ts +43 -117
  113. package/src/hooks/use-user-config.ts +1 -1
  114. package/src/hooks/use-workspace-config.ts +6 -2
  115. package/src/hooks/use-workspace-presence.ts +1 -1
  116. 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
+ }
@@ -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 {
@@ -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
  });
@@ -1081,6 +1081,45 @@ describe('useTaskActions', () => {
1081
1081
  priority: 'high',
1082
1082
  });
1083
1083
  });
1084
+
1085
+ it('keeps priority updates single-target when the context task is not selected', async () => {
1086
+ const task2: Task = { ...mockTask, id: 'task-2', name: 'Task 2' };
1087
+ const unselectedContextTask: Task = {
1088
+ ...mockTask,
1089
+ id: 'task-3',
1090
+ name: 'Task 3',
1091
+ };
1092
+ queryClient.setQueryData(
1093
+ ['tasks', 'board-1'],
1094
+ [mockTask, task2, unselectedContextTask]
1095
+ );
1096
+
1097
+ const { result } = renderHook(
1098
+ () =>
1099
+ useTaskActions({
1100
+ task: unselectedContextTask,
1101
+ boardId: 'board-1',
1102
+ targetCompletionList: mockCompletionList,
1103
+ targetClosedList: mockClosedList,
1104
+ availableLists: mockAvailableLists,
1105
+ onUpdate: vi.fn(),
1106
+ setIsLoading: vi.fn(),
1107
+ setMenuOpen: vi.fn(),
1108
+ selectedTasks: new Set(['task-1', 'task-2']),
1109
+ isMultiSelectMode: true,
1110
+ }),
1111
+ { wrapper }
1112
+ );
1113
+
1114
+ await act(async () => {
1115
+ await result.current.handlePriorityChange('high');
1116
+ });
1117
+
1118
+ expect(mockUpdateWorkspaceTask).toHaveBeenCalledTimes(1);
1119
+ expect(mockUpdateWorkspaceTask).toHaveBeenCalledWith('ws-1', 'task-3', {
1120
+ priority: 'high',
1121
+ });
1122
+ });
1084
1123
  });
1085
1124
 
1086
1125
  describe('updateEstimationPoints', () => {
@@ -1276,6 +1315,85 @@ describe('useTaskActions', () => {
1276
1315
  end_date: expect.any(String),
1277
1316
  });
1278
1317
  });
1318
+
1319
+ it('delegates selected-card custom dates to the bulk updater', async () => {
1320
+ const task2: Task = { ...mockTask, id: 'task-2', name: 'Task 2' };
1321
+ queryClient.setQueryData(['tasks', 'board-1'], [mockTask, task2]);
1322
+
1323
+ const bulkUpdateCustomDueDate = vi.fn().mockResolvedValue(undefined);
1324
+ const testDate = new Date('2024-01-01T00:00:00');
1325
+
1326
+ const { result } = renderHook(
1327
+ () =>
1328
+ useTaskActions({
1329
+ task: mockTask,
1330
+ boardId: 'board-1',
1331
+ targetCompletionList: mockCompletionList,
1332
+ targetClosedList: mockClosedList,
1333
+ availableLists: mockAvailableLists,
1334
+ onUpdate: vi.fn(),
1335
+ setIsLoading: vi.fn(),
1336
+ setMenuOpen: vi.fn(),
1337
+ setCustomDateDialogOpen: vi.fn(),
1338
+ selectedTasks: new Set(['task-1', 'task-2']),
1339
+ isMultiSelectMode: true,
1340
+ bulkUpdateCustomDueDate,
1341
+ }),
1342
+ { wrapper }
1343
+ );
1344
+
1345
+ await act(async () => {
1346
+ await result.current.handleCustomDateChange(testDate);
1347
+ });
1348
+
1349
+ expect(bulkUpdateCustomDueDate).toHaveBeenCalledWith(testDate);
1350
+ expect(mockUpdateWorkspaceTask).not.toHaveBeenCalled();
1351
+ });
1352
+
1353
+ it('keeps custom date updates single-target when the context task is not selected', async () => {
1354
+ const task2: Task = { ...mockTask, id: 'task-2', name: 'Task 2' };
1355
+ const unselectedContextTask: Task = {
1356
+ ...mockTask,
1357
+ id: 'task-3',
1358
+ name: 'Task 3',
1359
+ };
1360
+ queryClient.setQueryData(
1361
+ ['tasks', 'board-1'],
1362
+ [mockTask, task2, unselectedContextTask]
1363
+ );
1364
+
1365
+ const bulkUpdateCustomDueDate = vi.fn().mockResolvedValue(undefined);
1366
+ const testDate = new Date('2024-01-01T00:00:00');
1367
+
1368
+ const { result } = renderHook(
1369
+ () =>
1370
+ useTaskActions({
1371
+ task: unselectedContextTask,
1372
+ boardId: 'board-1',
1373
+ targetCompletionList: mockCompletionList,
1374
+ targetClosedList: mockClosedList,
1375
+ availableLists: mockAvailableLists,
1376
+ onUpdate: vi.fn(),
1377
+ setIsLoading: vi.fn(),
1378
+ setMenuOpen: vi.fn(),
1379
+ setCustomDateDialogOpen: vi.fn(),
1380
+ selectedTasks: new Set(['task-1', 'task-2']),
1381
+ isMultiSelectMode: true,
1382
+ bulkUpdateCustomDueDate,
1383
+ }),
1384
+ { wrapper }
1385
+ );
1386
+
1387
+ await act(async () => {
1388
+ await result.current.handleCustomDateChange(testDate);
1389
+ });
1390
+
1391
+ expect(bulkUpdateCustomDueDate).not.toHaveBeenCalled();
1392
+ expect(mockUpdateWorkspaceTask).toHaveBeenCalledTimes(1);
1393
+ expect(mockUpdateWorkspaceTask).toHaveBeenCalledWith('ws-1', 'task-3', {
1394
+ end_date: expect.any(String),
1395
+ });
1396
+ });
1279
1397
  });
1280
1398
 
1281
1399
  describe('handleToggleAssignee', () => {
@@ -0,0 +1,65 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { renderHook, waitFor } from '@testing-library/react';
3
+ import type { ReactNode } from 'react';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { useUserBooleanConfig } from '../use-user-config';
6
+
7
+ const { mockGetUserConfig, mockUpdateUserConfig } = vi.hoisted(() => ({
8
+ mockGetUserConfig: vi.fn(),
9
+ mockUpdateUserConfig: vi.fn(),
10
+ }));
11
+
12
+ vi.mock('@tuturuuu/internal-api/users', () => ({
13
+ getUserConfig: (...args: unknown[]) => mockGetUserConfig(...args),
14
+ updateUserConfig: (...args: unknown[]) => mockUpdateUserConfig(...args),
15
+ }));
16
+
17
+ function createWrapper() {
18
+ const queryClient = new QueryClient({
19
+ defaultOptions: {
20
+ queries: {
21
+ retry: false,
22
+ },
23
+ },
24
+ });
25
+
26
+ return function Wrapper({ children }: { children: ReactNode }) {
27
+ return (
28
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
29
+ );
30
+ };
31
+ }
32
+
33
+ describe('useUserBooleanConfig', () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+
38
+ it('uses the requested boolean default while the config is loading', () => {
39
+ mockGetUserConfig.mockReturnValue(new Promise(() => {}));
40
+
41
+ const { result } = renderHook(
42
+ () => useUserBooleanConfig('EXPAND_SETTINGS_ACCORDIONS', true),
43
+ { wrapper: createWrapper() }
44
+ );
45
+
46
+ expect(result.current.isLoading).toBe(true);
47
+ expect(result.current.value).toBe(true);
48
+ });
49
+
50
+ it('lets a saved false value override a default true value after loading', async () => {
51
+ mockGetUserConfig.mockResolvedValue({ value: 'false' });
52
+
53
+ const { result } = renderHook(
54
+ () => useUserBooleanConfig('EXPAND_SETTINGS_ACCORDIONS', true),
55
+ { wrapper: createWrapper() }
56
+ );
57
+
58
+ expect(result.current.value).toBe(true);
59
+
60
+ await waitFor(() => {
61
+ expect(result.current.isLoading).toBe(false);
62
+ expect(result.current.value).toBe(false);
63
+ });
64
+ });
65
+ });
@@ -20,7 +20,7 @@ vi.mock('@tuturuuu/supabase/next/client', () => ({
20
20
  createClient: createClientMock,
21
21
  }));
22
22
 
23
- vi.mock('@tuturuuu/internal-api', () => ({
23
+ vi.mock('@tuturuuu/internal-api/users', () => ({
24
24
  getCurrentUserProfile: getCurrentUserProfileMock,
25
25
  }));
26
26