@tuturuuu/ui 0.13.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/biome.json +1 -1
  3. package/package.json +62 -62
  4. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +12 -0
  5. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +93 -6
  6. package/src/components/ui/custom/loading-statistic-card.tsx +18 -0
  7. package/src/components/ui/custom/notification-popover-client.tsx +5 -1
  8. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.test.tsx +34 -0
  9. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +106 -0
  10. package/src/components/ui/custom/settings/sidebar-settings.tsx +18 -6
  11. package/src/components/ui/custom/settings/task-settings.tsx +55 -0
  12. package/src/components/ui/custom/settings-dialog-shell.tsx +18 -12
  13. package/src/components/ui/custom/sidebar-context.tsx +58 -3
  14. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +5 -3
  15. package/src/components/ui/custom/statistic-card.tsx +104 -0
  16. package/src/components/ui/custom/structure.test.tsx +29 -0
  17. package/src/components/ui/custom/structure.tsx +103 -88
  18. package/src/components/ui/finance/invoices/civil-month.ts +224 -0
  19. package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.test.tsx +49 -0
  20. package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.tsx +81 -0
  21. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
  22. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +90 -2
  23. package/src/components/ui/finance/invoices/new-invoice-page.tsx +40 -4
  24. package/src/components/ui/finance/invoices/pending-columns.test.ts +26 -0
  25. package/src/components/ui/finance/invoices/pending-columns.tsx +45 -14
  26. package/src/components/ui/finance/invoices/standard-invoice.test.ts +31 -0
  27. package/src/components/ui/finance/invoices/standard-invoice.tsx +33 -16
  28. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +331 -0
  29. package/src/components/ui/finance/invoices/subscription-invoice.tsx +53 -28
  30. package/src/components/ui/finance/invoices/utils.test.ts +84 -0
  31. package/src/components/ui/finance/invoices/utils.ts +56 -134
  32. package/src/components/ui/resizable.test.ts +15 -0
  33. package/src/components/ui/resizable.tsx +28 -6
  34. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +4 -0
  35. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +62 -0
  36. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.test.ts +118 -0
  37. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.ts +144 -70
  38. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +48 -0
  39. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +50 -6
  40. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +6 -0
  41. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +4 -2
  42. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +41 -3
  43. package/src/components/ui/tu-do/boards/boardId/menus/task-due-date-menu.tsx +31 -10
  44. package/src/components/ui/tu-do/boards/boardId/menus/task-priority-menu.tsx +7 -5
  45. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +7 -5
  46. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.test.ts +44 -0
  47. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.ts +17 -0
  48. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +2 -0
  49. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -0
  50. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +44 -30
  51. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +54 -0
  52. package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +43 -1
  53. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +33 -15
  54. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +3 -3
  55. package/src/components/ui/tu-do/progress/task-progress-page.tsx +74 -28
  56. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +12 -10
  57. package/src/components/ui/tu-do/projects/components/project-grid-card.tsx +22 -6
  58. package/src/components/ui/tu-do/projects/components/project-list-item.tsx +24 -8
  59. package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx +6 -0
  60. package/src/components/ui/tu-do/projects/projectId/components/tasks-tab.tsx +3 -1
  61. package/src/components/ui/tu-do/projects/projectId/task-project-detail-page-client.tsx +11 -3
  62. package/src/components/ui/tu-do/projects/projectId/task-project-detail.tsx +2 -0
  63. package/src/components/ui/tu-do/projects/projectId/types.ts +1 -0
  64. package/src/components/ui/tu-do/projects/task-projects-client.tsx +45 -1
  65. package/src/components/ui/tu-do/projects/types.ts +4 -1
  66. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +10 -0
  67. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -1
  68. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +87 -2
  69. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +70 -0
  70. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +27 -3
  71. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +41 -3
  72. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +54 -0
  73. package/src/components/ui/tu-do/shared/board-header.tsx +20 -0
  74. package/src/components/ui/tu-do/shared/board-views.tsx +33 -3
  75. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
  76. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +2 -0
  77. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +7 -0
  78. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-mutations.test.tsx +231 -0
  79. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +139 -68
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.test.tsx +68 -0
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +17 -0
  82. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +45 -23
  83. package/src/components/ui/tu-do/shared/task-quick-create-target-list.ts +20 -0
  84. package/src/components/ui/tu-do/shared/task-row-actions-menu.test.tsx +138 -0
  85. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +15 -9
  86. package/src/components/ui/tu-do/templates/client.tsx +97 -78
  87. package/src/components/ui/tu-do/templates/marketplace/client.tsx +171 -135
  88. package/src/components/ui/tu-do/templates/task-templates-hub.tsx +3 -0
  89. package/src/components/ui/tu-do/templates/templateId/client.tsx +24 -9
  90. package/src/components/ui/tu-do/templates/templateId/task-template-detail-page-client.tsx +8 -2
  91. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -0
  92. package/src/hooks/__tests__/use-task-actions.test.tsx +14 -39
  93. package/src/hooks/task-actions-personal-external.ts +12 -6
  94. package/src/hooks/use-notifications.ts +14 -2
  95. package/src/hooks/use-user-config.ts +10 -2
  96. package/vitest.config.ts +4 -0
@@ -17,7 +17,9 @@ import { getBoardConfigKey } from '../board-config-storage';
17
17
  import { BoardViews } from '../board-views';
18
18
 
19
19
  const listWorkspaceTasksMock = vi.hoisted(() => vi.fn());
20
+ const getUserConfigMock = vi.hoisted(() => vi.fn());
20
21
  const getUserWorkspaceConfigMock = vi.hoisted(() => vi.fn());
22
+ const updateUserConfigMock = vi.hoisted(() => vi.fn());
21
23
  const updateUserWorkspaceConfigMock = vi.hoisted(() => vi.fn());
22
24
  const createTaskMock = vi.fn();
23
25
  const loadListPageMock = vi.fn();
@@ -64,8 +66,11 @@ vi.mock('@tuturuuu/internal-api/tasks', () => ({
64
66
  vi.mock('@tuturuuu/internal-api/users', () => ({
65
67
  TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID: 'TASK_BOARD_PINNED_SPECIAL_LISTS',
66
68
  TASK_LAST_BOARD_VIEW_CONFIG_ID: 'TASK_LAST_BOARD_VIEW',
69
+ TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID: 'TASK_QUICK_CREATE_TARGET_LIST',
70
+ getUserConfig: (...args: unknown[]) => getUserConfigMock(...args),
67
71
  getUserWorkspaceConfig: (...args: unknown[]) =>
68
72
  getUserWorkspaceConfigMock(...args),
73
+ updateUserConfig: (...args: unknown[]) => updateUserConfigMock(...args),
69
74
  updateUserWorkspaceConfig: (...args: unknown[]) =>
70
75
  updateUserWorkspaceConfigMock(...args),
71
76
  }));
@@ -260,8 +265,12 @@ describe('BoardViews', () => {
260
265
  progressivePagination = {};
261
266
  listWorkspaceTasksMock.mockReset();
262
267
  listWorkspaceTasksMock.mockResolvedValue({ tasks: mockTasks });
268
+ getUserConfigMock.mockReset();
269
+ getUserConfigMock.mockResolvedValue({ value: null });
263
270
  getUserWorkspaceConfigMock.mockReset();
264
271
  getUserWorkspaceConfigMock.mockResolvedValue({ value: null });
272
+ updateUserConfigMock.mockReset();
273
+ updateUserConfigMock.mockResolvedValue({ message: 'ok' });
265
274
  updateUserWorkspaceConfigMock.mockReset();
266
275
  updateUserWorkspaceConfigMock.mockResolvedValue({ message: 'ok' });
267
276
  window.localStorage.clear();
@@ -312,6 +321,7 @@ describe('BoardViews', () => {
312
321
  expect(boardHeaderProps?.titlePrefix).toBeDefined();
313
322
  expect(kanbanBoardProps?.readOnly).toBe(true);
314
323
  expect(listWorkspaceTasksMock).not.toHaveBeenCalled();
324
+ expect(getUserConfigMock).not.toHaveBeenCalled();
315
325
  });
316
326
 
317
327
  it('enables assignees for personal boards that have guest access', async () => {
@@ -544,6 +554,66 @@ describe('BoardViews', () => {
544
554
  );
545
555
  });
546
556
 
557
+ it('creates a task in the hovered list when the quick-create target prefers hovered lists', async () => {
558
+ getUserConfigMock.mockResolvedValue({ value: 'hovered_list' });
559
+ const { queryClient } = renderBoardViews({
560
+ board: { ...mockBoard, default_list_id: 'list-1' },
561
+ });
562
+
563
+ await waitFor(() => {
564
+ expect(
565
+ queryClient.getQueryData([
566
+ 'user-config',
567
+ 'TASK_QUICK_CREATE_TARGET_LIST',
568
+ ])
569
+ ).toBe('hovered_list');
570
+ });
571
+
572
+ await act(async () => {
573
+ kanbanBoardProps?.onHoveredTaskListChange?.('list-2');
574
+ });
575
+
576
+ fireEvent.keyDown(document, { key: 'c' });
577
+
578
+ expect(createTaskMock).toHaveBeenCalledTimes(1);
579
+ expect(createTaskMock).toHaveBeenCalledWith(
580
+ 'board-1',
581
+ 'list-2',
582
+ mockLists,
583
+ expect.objectContaining({ labels: [] })
584
+ );
585
+ });
586
+
587
+ it('falls back to the board default list in hovered-list mode when no hovered list is selectable', async () => {
588
+ getUserConfigMock.mockResolvedValue({ value: 'hovered_list' });
589
+ const { queryClient } = renderBoardViews({
590
+ board: { ...mockBoard, default_list_id: 'list-2' },
591
+ });
592
+
593
+ await waitFor(() => {
594
+ expect(
595
+ queryClient.getQueryData([
596
+ 'user-config',
597
+ 'TASK_QUICK_CREATE_TARGET_LIST',
598
+ ])
599
+ ).toBe('hovered_list');
600
+ });
601
+
602
+ await act(async () => {
603
+ kanbanBoardProps?.onHoveredTaskListChange?.('external-list');
604
+ });
605
+
606
+ fireEvent.keyDown(document, { key: 'c' });
607
+
608
+ expect(createTaskMock).toHaveBeenCalledTimes(1);
609
+ expect(createTaskMock).toHaveBeenCalledWith(
610
+ 'board-1',
611
+ 'list-2',
612
+ mockLists,
613
+ expect.objectContaining({ labels: [] })
614
+ );
615
+ });
616
+
547
617
  it('falls back to the first list when the default list is unavailable', () => {
548
618
  renderBoardViews({
549
619
  board: { ...mockBoard, default_list_id: 'list-does-not-exist' },
@@ -8,11 +8,17 @@ describe('TaskBoardLoadingState', () => {
8
8
  render(<TaskBoardLoadingState root />);
9
9
 
10
10
  expect(screen.getByTestId('task-board-loading-state')).toHaveClass(
11
- '-m-4',
11
+ '-mt-4',
12
+ '-mb-4',
13
+ '-ml-4',
12
14
  'h-[calc(100dvh+2rem)]',
13
15
  'w-[calc(100%+2rem)]',
14
16
  'bg-transparent'
15
17
  );
18
+ expect(screen.getByTestId('task-board-loading-state')).not.toHaveClass(
19
+ '-m-4',
20
+ '-mr-4'
21
+ );
16
22
  expect(screen.getByTestId('kanban-skeleton')).toHaveClass('bg-transparent');
17
23
  expect(screen.getByTestId('kanban-skeleton-frame')).toHaveClass(
18
24
  'py-2',
@@ -34,6 +40,9 @@ describe('TaskBoardLoadingState', () => {
34
40
  );
35
41
  expect(screen.getByTestId('task-board-loading-state')).not.toHaveClass(
36
42
  '-m-4',
43
+ '-mt-4',
44
+ '-mb-4',
45
+ '-ml-4',
37
46
  'w-[calc(100%+2rem)]'
38
47
  );
39
48
  expect(screen.getByTestId('kanban-skeleton-frame')).toHaveClass('p-2');
@@ -46,22 +55,37 @@ describe('TaskBoardLoadingState', () => {
46
55
  render(<TaskBoardLoadingState root showHeader />);
47
56
 
48
57
  expect(screen.getByTestId('task-board-loading-state')).toHaveClass(
49
- '-m-4',
58
+ '-mt-4',
59
+ '-mb-4',
60
+ '-ml-4',
50
61
  'h-[calc(100dvh+2rem)]',
51
62
  'w-[calc(100%+2rem)]',
52
63
  'flex',
53
64
  'flex-col'
54
65
  );
66
+ expect(screen.getByTestId('task-board-loading-state')).not.toHaveClass(
67
+ '-m-4',
68
+ '-mr-4'
69
+ );
55
70
  expect(screen.getByTestId('task-board-header-skeleton')).toHaveAttribute(
56
71
  'aria-hidden',
57
72
  'true'
58
73
  );
59
74
  expect(screen.getByTestId('task-board-header-skeleton')).toHaveClass(
60
75
  'border-b',
61
- 'px-2',
76
+ 'pl-2',
77
+ 'pr-0',
62
78
  'pt-2',
63
79
  'pb-2'
64
80
  );
81
+ expect(screen.getByTestId('task-board-header-skeleton')).not.toHaveClass(
82
+ 'px-2'
83
+ );
84
+ expect(screen.getByTestId('kanban-skeleton-frame')).toHaveClass(
85
+ 'py-2',
86
+ 'pl-2',
87
+ 'pr-0'
88
+ );
65
89
  expect(screen.getByTestId('task-board-header-skeleton')).not.toHaveClass(
66
90
  '-mt-2'
67
91
  );
@@ -171,6 +171,8 @@ vi.mock('../task-edit-dialog', () => ({
171
171
  isHydratingTask,
172
172
  taskLoadError,
173
173
  task,
174
+ visibleBoardId,
175
+ visibleTaskSnapshot,
174
176
  onClose,
175
177
  onNavigateToTask,
176
178
  }: {
@@ -179,6 +181,8 @@ vi.mock('../task-edit-dialog', () => ({
179
181
  isHydratingTask?: boolean;
180
182
  taskLoadError?: boolean;
181
183
  task?: Task;
184
+ visibleBoardId?: string;
185
+ visibleTaskSnapshot?: Partial<Task>;
182
186
  onClose: () => void;
183
187
  onNavigateToTask?: (taskId: string) => Promise<void>;
184
188
  }) => {
@@ -197,6 +201,8 @@ vi.mock('../task-edit-dialog', () => ({
197
201
  data-hydrating={String(!!isHydratingTask)}
198
202
  data-load-error={String(!!taskLoadError)}
199
203
  data-open={isOpen}
204
+ data-visible-board-id={visibleBoardId}
205
+ data-visible-list-id={visibleTaskSnapshot?.list_id}
200
206
  >
201
207
  {task && <div data-testid="task-name">{task.name}</div>}
202
208
  <button
@@ -684,11 +690,25 @@ describe('TaskDialogManager', () => {
684
690
  const { openTaskById } = useTaskDialogContext();
685
691
 
686
692
  React.useEffect(() => {
693
+ const visibleExternalSnapshot = {
694
+ ...mockTask,
695
+ id: 'external-task-1',
696
+ name: 'External snapshot',
697
+ list_id: 'personal-list-1',
698
+ personal_board_id: 'personal-board-1',
699
+ personal_list_id: 'personal-list-1',
700
+ personal_sort_key: 2000,
701
+ source_workspace_id: 'source-workspace-1',
702
+ source_board_id: 'external-board-1',
703
+ source_list_id: 'external-list-1',
704
+ source_list_name: 'External list',
705
+ is_personal_external: true,
706
+ } satisfies Task;
707
+
687
708
  void openTaskById('external-task-1', {
688
709
  initialTask: {
689
- ...mockTask,
690
- id: 'external-task-1',
691
- name: 'External snapshot',
710
+ ...visibleExternalSnapshot,
711
+ list_id: 'external-list-1',
692
712
  },
693
713
  boardId: 'external-board-1',
694
714
  availableLists: [
@@ -700,6 +720,8 @@ describe('TaskDialogManager', () => {
700
720
  ],
701
721
  taskWsId: 'source-workspace-1',
702
722
  taskWorkspacePersonal: false,
723
+ visibleBoardId: 'personal-board-1',
724
+ visibleTaskSnapshot: visibleExternalSnapshot,
703
725
  });
704
726
  }, [openTaskById]);
705
727
 
@@ -719,6 +741,14 @@ describe('TaskDialogManager', () => {
719
741
  );
720
742
  expect(getByTestId('task-name')).toHaveTextContent('External snapshot');
721
743
  });
744
+ expect(getByTestId('task-edit-dialog')).toHaveAttribute(
745
+ 'data-visible-board-id',
746
+ 'personal-board-1'
747
+ );
748
+ expect(getByTestId('task-edit-dialog')).toHaveAttribute(
749
+ 'data-visible-list-id',
750
+ 'personal-list-1'
751
+ );
722
752
  expect(taskDialogRenderStats).toMatchObject({
723
753
  mounts: 1,
724
754
  unmounts: 0,
@@ -751,6 +781,14 @@ describe('TaskDialogManager', () => {
751
781
  'Hydrated external task'
752
782
  );
753
783
  });
784
+ expect(getByTestId('task-edit-dialog')).toHaveAttribute(
785
+ 'data-visible-board-id',
786
+ 'personal-board-1'
787
+ );
788
+ expect(getByTestId('task-edit-dialog')).toHaveAttribute(
789
+ 'data-visible-list-id',
790
+ 'personal-list-1'
791
+ );
754
792
  expect(taskDialogRenderStats).toMatchObject({
755
793
  mounts: 1,
756
794
  unmounts: 0,
@@ -515,6 +515,60 @@ describe('useProgressiveBoardLoader', () => {
515
515
  ]);
516
516
  });
517
517
 
518
+ it('preserves a freshly property-updated external overlay when list revalidation misses it', async () => {
519
+ const propertyUpdatedExternalTask = {
520
+ id: 'task-external',
521
+ display_number: 43,
522
+ name: 'Updated external task',
523
+ list_id: 'personal-list-2',
524
+ created_at: '2026-05-07T00:00:00.000Z',
525
+ sort_key: 1_500_000,
526
+ personal_board_id: 'board-1',
527
+ personal_list_id: 'personal-list-2',
528
+ personal_sort_key: 1_500_000,
529
+ source_workspace_id: 'source-ws',
530
+ source_board_id: 'source-board',
531
+ source_list_id: 'source-list',
532
+ source_list_name: 'Source list',
533
+ source_list_status: 'active',
534
+ is_personal_external: true,
535
+ priority: 'high',
536
+ _localMutationAt: Date.now(),
537
+ } as Task;
538
+
539
+ const { result } = renderHook(
540
+ () => useProgressiveBoardLoader('personal', 'board-1'),
541
+ { wrapper }
542
+ );
543
+
544
+ vi.mocked(listWorkspaceTasks).mockResolvedValueOnce({ tasks: [] });
545
+
546
+ await act(async () => {
547
+ await result.current.loadListPage('personal-list-2', 0);
548
+ });
549
+
550
+ queryClient.setQueryData(
551
+ ['tasks', 'board-1'],
552
+ [propertyUpdatedExternalTask]
553
+ );
554
+ vi.mocked(listWorkspaceTasks).mockResolvedValueOnce({ tasks: [] });
555
+
556
+ await act(async () => {
557
+ await result.current.revalidateLoadedLists();
558
+ });
559
+
560
+ expect(queryClient.getQueryData<Task[]>(['tasks', 'board-1'])).toEqual([
561
+ expect.objectContaining({
562
+ id: 'task-external',
563
+ priority: 'high',
564
+ list_id: 'personal-list-2',
565
+ personal_list_id: 'personal-list-2',
566
+ source_list_id: 'source-list',
567
+ _localMutationAt: expect.any(Number),
568
+ }),
569
+ ]);
570
+ });
571
+
518
572
  it('revalidates loaded lists without clearing other list tasks', async () => {
519
573
  const existingOtherListTask: Task = {
520
574
  id: 'task-other',
@@ -15,6 +15,7 @@ import {
15
15
  LayoutGrid,
16
16
  List,
17
17
  Loader2,
18
+ PanelLeftClose,
18
19
  Pencil,
19
20
  Play,
20
21
  Search,
@@ -29,6 +30,7 @@ import type { WorkspaceTaskBoard } from '@tuturuuu/types';
29
30
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
30
31
  import { Button } from '@tuturuuu/ui/button';
31
32
  import { Combobox } from '@tuturuuu/ui/custom/combobox';
33
+ import { useOptionalSidebar } from '@tuturuuu/ui/custom/sidebar-context';
32
34
  import { Input } from '@tuturuuu/ui/input';
33
35
  import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
34
36
  import { cn } from '@tuturuuu/utils/format';
@@ -157,6 +159,9 @@ export function BoardHeader({
157
159
  !publicView &&
158
160
  isPersonalWorkspace &&
159
161
  currentView === 'kanban';
162
+ const sidebar = useOptionalSidebar();
163
+ const hideSidebarVisible =
164
+ Boolean(sidebar) && sidebar?.behavior !== 'hidden' && !publicView;
160
165
  const hasSharedBoardGuests =
161
166
  board.access_type === 'guest' || board.has_guest_access === true;
162
167
  const presenceVisible =
@@ -568,6 +573,21 @@ export function BoardHeader({
568
573
  />
569
574
  )}
570
575
 
576
+ {hideSidebarVisible && (
577
+ <ToolbarTooltip label={t('common.hide_sidebar')}>
578
+ <Button
579
+ type="button"
580
+ size="xs"
581
+ variant="outline"
582
+ className={toolbarButtonClass}
583
+ onClick={() => sidebar?.handleBehaviorChange('hidden')}
584
+ aria-label={t('common.hide_sidebar')}
585
+ >
586
+ <PanelLeftClose className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
587
+ </Button>
588
+ </ToolbarTooltip>
589
+ )}
590
+
571
591
  {/* Smart Focus Button */}
572
592
  {interactiveControlsVisible && (
573
593
  <ToolbarTooltip
@@ -13,6 +13,7 @@ import {
13
13
  import {
14
14
  TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
15
15
  TASK_LAST_BOARD_VIEW_CONFIG_ID,
16
+ TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID,
16
17
  } from '@tuturuuu/internal-api/users';
17
18
  import type {
18
19
  Workspace,
@@ -35,6 +36,7 @@ import {
35
36
  useMemo,
36
37
  useState,
37
38
  } from 'react';
39
+ import { useUserConfig } from '../../../../hooks/use-user-config';
38
40
  import {
39
41
  useUpdateUserWorkspaceConfig,
40
42
  useUserWorkspaceConfig,
@@ -58,6 +60,10 @@ import {
58
60
  type SpecialTaskListPin,
59
61
  serializeSpecialTaskListPins,
60
62
  } from './special-task-list-pins';
63
+ import {
64
+ DEFAULT_TASK_QUICK_CREATE_TARGET_LIST,
65
+ normalizeTaskQuickCreateTargetList,
66
+ } from './task-quick-create-target-list';
61
67
 
62
68
  export type ViewType =
63
69
  | 'kanban'
@@ -290,8 +296,19 @@ export function BoardViews({
290
296
  const [isMultiSelectMode, setIsMultiSelectMode] = useState(false);
291
297
  const [kanbanBulkSelectionActive, setKanbanBulkSelectionActive] =
292
298
  useState(false);
299
+ const [hoveredTaskListId, setHoveredTaskListId] = useState<string | null>(
300
+ null
301
+ );
293
302
  const { createTask } = useTaskDialog();
294
303
  const localTaskState = readOnly || publicView;
304
+ const { data: quickCreateTargetListRaw } = useUserConfig(
305
+ TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID,
306
+ DEFAULT_TASK_QUICK_CREATE_TARGET_LIST,
307
+ { enabled: !localTaskState }
308
+ );
309
+ const quickCreateTargetList = normalizeTaskQuickCreateTargetList(
310
+ quickCreateTargetListRaw
311
+ );
295
312
  const boardAssigneesEnabled =
296
313
  !workspace.personal ||
297
314
  board.access_type === 'guest' ||
@@ -820,6 +837,15 @@ export function BoardViews({
820
837
  statusFilteredLists,
821
838
  ]);
822
839
 
840
+ useEffect(() => {
841
+ if (!hoveredTaskListId) return;
842
+
843
+ const stillSelectable = filteredLists.some(
844
+ (list) => list.id === hoveredTaskListId && !list.is_external_staging
845
+ );
846
+ if (!stillSelectable) setHoveredTaskListId(null);
847
+ }, [filteredLists, hoveredTaskListId]);
848
+
823
849
  const sourceTasks = useMemo(() => {
824
850
  if (localTaskState) return locallyFilteredTasks;
825
851
 
@@ -902,11 +928,14 @@ export function BoardViews({
902
928
  const selectableLists = filteredLists.filter(
903
929
  (list) => !list.is_external_staging
904
930
  );
905
- // Prefer the board's configured default list for new tasks, falling back
906
- // to the first selectable list when unset or the list is unavailable.
907
- const targetList =
931
+ const defaultTargetList =
908
932
  selectableLists.find((list) => list.id === board.default_list_id) ??
909
933
  selectableLists[0];
934
+ const hoveredTargetList =
935
+ quickCreateTargetList === 'hovered_list'
936
+ ? selectableLists.find((list) => list.id === hoveredTaskListId)
937
+ : undefined;
938
+ const targetList = hoveredTargetList ?? defaultTargetList;
910
939
  if (!targetList) return;
911
940
  createTask(board.id, targetList.id, selectableLists, filters);
912
941
  },
@@ -1022,6 +1051,7 @@ export function BoardViews({
1022
1051
  onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
1023
1052
  canUseBoardAssignees={boardAssigneesEnabled}
1024
1053
  assigneeMemberSource={assigneeMemberSource}
1054
+ onHoveredTaskListChange={setHoveredTaskListId}
1025
1055
  readOnly={readOnly}
1026
1056
  />
1027
1057
  );
@@ -13,11 +13,11 @@ const HEADER_ACTIONS = [
13
13
  'settings',
14
14
  ];
15
15
 
16
- function TaskBoardHeaderSkeleton() {
16
+ function TaskBoardHeaderSkeleton({ root = false }: { root?: boolean }) {
17
17
  return (
18
18
  <div
19
19
  aria-hidden="true"
20
- className="border-b px-2 pt-2 pb-2"
20
+ className={cn('border-b pt-2 pb-2', root ? 'pr-0 pl-2' : 'px-2')}
21
21
  data-testid="task-board-header-skeleton"
22
22
  >
23
23
  <div className="flex flex-wrap items-center justify-between gap-1.5 sm:gap-2">
@@ -57,7 +57,7 @@ export function TaskBoardLoadingState({
57
57
  className={cn(
58
58
  'overflow-hidden bg-transparent',
59
59
  root
60
- ? '-m-4 h-[calc(100dvh+2rem)] min-h-[calc(32rem+2rem)] w-[calc(100%+2rem)] min-w-[calc(100%+2rem)]'
60
+ ? '-mt-4 -mb-4 -ml-4 h-[calc(100dvh+2rem)] min-h-[calc(32rem+2rem)] w-[calc(100%+2rem)] min-w-[calc(100%+2rem)]'
61
61
  : 'h-[calc(100dvh-1rem)] min-h-[32rem] w-full',
62
62
  showHeader && 'flex flex-col',
63
63
  className
@@ -66,7 +66,7 @@ export function TaskBoardLoadingState({
66
66
  >
67
67
  {showHeader ? (
68
68
  <>
69
- <TaskBoardHeaderSkeleton />
69
+ <TaskBoardHeaderSkeleton root={root} />
70
70
  <div
71
71
  className="min-h-0 flex-1 overflow-hidden"
72
72
  data-testid="task-board-loading-body"
@@ -542,6 +542,8 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
542
542
  taskWsId={state.taskWsId}
543
543
  task={state.task}
544
544
  boardId={state.boardId || ''}
545
+ visibleBoardId={state.visibleBoardId}
546
+ visibleTaskSnapshot={state.visibleTaskSnapshot}
545
547
  isOpen={state.isOpen}
546
548
  availableLists={state.availableLists}
547
549
  sharedContext={
@@ -220,6 +220,9 @@ interface TaskDialogHeaderProps {
220
220
  isPersonalWorkspace?: boolean;
221
221
  /** Callback to open share dialog */
222
222
  onOpenShareDialog?: () => void;
223
+ /** Optional archive action for document-type tasks. */
224
+ onArchiveTask?: () => void;
225
+ archiveTaskDisabled?: boolean;
223
226
  /** Whether the dialog is in read-only mode */
224
227
  disabled?: boolean;
225
228
  /** Whether task-dependent actions should be disabled while the task hydrates. */
@@ -263,6 +266,8 @@ export function TaskDialogHeader({
263
266
  onNavigateBack,
264
267
  isPersonalWorkspace = false,
265
268
  onOpenShareDialog,
269
+ onArchiveTask,
270
+ archiveTaskDisabled = false,
266
271
  disabled = false,
267
272
  controlsDisabled = false,
268
273
  onScrollToUserCursor,
@@ -487,6 +492,8 @@ export function TaskDialogHeader({
487
492
  onShowDeleteDialog={() => setShowDeleteConfirm(true)}
488
493
  onClearDraft={clearDraftState}
489
494
  onNavigateBack={onNavigateBack}
495
+ onArchiveTask={onArchiveTask}
496
+ archiveTaskDisabled={archiveTaskDisabled}
490
497
  onOpenShareDialog={onOpenShareDialog}
491
498
  disabled={disabled}
492
499
  controlsDisabled={controlsDisabled}