@tuturuuu/ui 0.9.0 → 0.10.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 (94) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +6 -5
  3. package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
  4. package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
  5. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
  6. package/src/components/ui/custom/nav-link.test.tsx +165 -0
  7. package/src/components/ui/custom/nav-link.tsx +69 -11
  8. package/src/components/ui/custom/navigation.tsx +1 -0
  9. package/src/components/ui/custom/settings/task-settings.tsx +104 -0
  10. package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
  11. package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
  12. package/src/components/ui/custom/settings-dialog-search.ts +75 -0
  13. package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
  14. package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
  15. package/src/components/ui/custom/workspace-select.tsx +17 -16
  16. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +16 -0
  17. package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
  18. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
  19. package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
  20. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
  21. package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
  22. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
  23. package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
  24. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
  25. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
  26. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
  27. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
  28. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
  29. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
  30. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
  31. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
  32. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
  33. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
  34. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
  35. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
  36. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
  37. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
  38. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
  39. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
  40. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
  41. package/src/components/ui/tu-do/boards/form.tsx +1 -1
  42. package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
  43. package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
  44. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
  45. package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
  46. package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
  47. package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
  48. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
  49. package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
  50. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
  51. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
  52. package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
  53. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +116 -1
  54. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
  55. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
  56. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
  57. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +21 -0
  58. package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
  59. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
  60. package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
  61. package/src/components/ui/tu-do/shared/board-client.tsx +14 -4
  62. package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
  63. package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
  64. package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
  65. package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
  66. package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
  67. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
  68. package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
  69. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
  70. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
  71. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
  72. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
  73. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
  74. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
  75. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
  76. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
  77. package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
  78. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
  79. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
  82. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
  83. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
  87. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +9 -0
  88. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
  89. package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
  90. package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
  91. package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
  92. package/src/hooks/useBoardPresence.ts +364 -0
  93. package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
  94. package/src/lib/workspace-actions.ts +2 -6
@@ -382,6 +382,44 @@ describe('BoardHeader', () => {
382
382
  ).not.toBeInTheDocument();
383
383
  });
384
384
 
385
+ it('shows presence avatars for workspace boards', () => {
386
+ renderBoardHeader();
387
+
388
+ expect(screen.getByTestId('board-user-presence')).toBeInTheDocument();
389
+ });
390
+
391
+ it('hides presence avatars for unshared personal boards', () => {
392
+ renderBoardHeader({
393
+ isPersonalWorkspace: true,
394
+ });
395
+
396
+ expect(screen.queryByTestId('board-user-presence')).not.toBeInTheDocument();
397
+ });
398
+
399
+ it('shows presence avatars for personal boards shared with guests', () => {
400
+ renderBoardHeader({
401
+ board: {
402
+ ...mockBoard,
403
+ has_guest_access: true,
404
+ },
405
+ isPersonalWorkspace: true,
406
+ });
407
+
408
+ expect(screen.getByTestId('board-user-presence')).toBeInTheDocument();
409
+ });
410
+
411
+ it('shows presence avatars for direct board guest access', () => {
412
+ renderBoardHeader({
413
+ board: {
414
+ ...mockBoard,
415
+ access_type: 'guest',
416
+ },
417
+ isPersonalWorkspace: true,
418
+ });
419
+
420
+ expect(screen.getByTestId('board-user-presence')).toBeInTheDocument();
421
+ });
422
+
385
423
  it('updates status, view, and sort through combobox controls', () => {
386
424
  const onFiltersChange = vi.fn();
387
425
  const onListStatusFilterChange = vi.fn();
@@ -7,7 +7,7 @@ import { BoardSwitcher } from '../board-switcher';
7
7
  const {
8
8
  createWorkspaceTaskBoardMock,
9
9
  isTaskRememberLastBoardEnabledMock,
10
- listWorkspaceTaskBoardsMock,
10
+ listCurrentUserTaskBoardsMock,
11
11
  pushMock,
12
12
  rememberLastBoardConfig,
13
13
  updateUserWorkspaceConfigMock,
@@ -17,7 +17,7 @@ const {
17
17
  isTaskRememberLastBoardEnabledMock: vi.fn(
18
18
  (value: string | null | undefined) => value !== 'false'
19
19
  ),
20
- listWorkspaceTaskBoardsMock: vi.fn(),
20
+ listCurrentUserTaskBoardsMock: vi.fn(),
21
21
  pushMock: vi.fn(),
22
22
  rememberLastBoardConfig: {
23
23
  value: 'true' as string | null | undefined,
@@ -32,7 +32,14 @@ let comboboxProps:
32
32
  creatingText?: string;
33
33
  onChange: (value: string) => void;
34
34
  onCreate?: (value: string) => Promise<{ label: string; value: string }>;
35
- options: Array<{ icon?: unknown; label: string; value: string }>;
35
+ options: Array<{
36
+ badge?: unknown;
37
+ description?: string;
38
+ group?: string;
39
+ icon?: unknown;
40
+ label: string;
41
+ value: string;
42
+ }>;
36
43
  searchPlaceholder?: string;
37
44
  selected?: string;
38
45
  showSelectedIcon?: boolean;
@@ -43,9 +50,9 @@ vi.mock('@tuturuuu/internal-api/tasks', () => ({
43
50
  createWorkspaceTaskBoard: (
44
51
  ...args: Parameters<typeof createWorkspaceTaskBoardMock>
45
52
  ) => createWorkspaceTaskBoardMock(...args),
46
- listWorkspaceTaskBoards: (
47
- ...args: Parameters<typeof listWorkspaceTaskBoardsMock>
48
- ) => listWorkspaceTaskBoardsMock(...args),
53
+ listCurrentUserTaskBoards: (
54
+ ...args: Parameters<typeof listCurrentUserTaskBoardsMock>
55
+ ) => listCurrentUserTaskBoardsMock(...args),
49
56
  }));
50
57
 
51
58
  vi.mock('@tuturuuu/internal-api/users', () => ({
@@ -150,15 +157,25 @@ describe('BoardSwitcher', () => {
150
157
  vi.clearAllMocks();
151
158
  comboboxProps = undefined;
152
159
  rememberLastBoardConfig.value = 'true';
153
- listWorkspaceTaskBoardsMock.mockResolvedValue({
160
+ listCurrentUserTaskBoardsMock.mockResolvedValue({
154
161
  boards: [
155
162
  {
163
+ access_type: 'member',
156
164
  archived_at: null,
157
165
  created_at: '2026-06-01T00:00:00.000Z',
158
166
  deleted_at: null,
159
167
  icon: null,
160
168
  id: 'board-2',
161
169
  name: 'Roadmap',
170
+ ticket_prefix: null,
171
+ workspace: {
172
+ avatar_url: null,
173
+ id: 'ws-1',
174
+ logo_url: null,
175
+ name: 'Current Workspace',
176
+ personal: false,
177
+ },
178
+ ws_id: 'ws-1',
162
179
  },
163
180
  ],
164
181
  });
@@ -188,6 +205,12 @@ describe('BoardSwitcher', () => {
188
205
  });
189
206
  expect(comboboxProps?.showSelectedIcon).toBeUndefined();
190
207
  expect(comboboxProps?.options.some((option) => option.icon)).toBe(true);
208
+ const activeBoardOption = comboboxProps?.options.find(
209
+ (option) => option.value === 'board-2'
210
+ );
211
+ expect(activeBoardOption?.badge).toBeUndefined();
212
+ expect(activeBoardOption?.description).toBeUndefined();
213
+ expect(activeBoardOption?.group).toBe('Current Workspace');
191
214
 
192
215
  fireEvent.click(screen.getByTestId('board-combobox'));
193
216
 
@@ -205,6 +228,69 @@ describe('BoardSwitcher', () => {
205
228
  });
206
229
  });
207
230
 
231
+ it('orders current workspace boards first and switches across workspaces without updating defaults', async () => {
232
+ listCurrentUserTaskBoardsMock.mockResolvedValue({
233
+ boards: [
234
+ {
235
+ access_type: 'member',
236
+ archived_at: null,
237
+ created_at: '2026-06-01T00:00:00.000Z',
238
+ deleted_at: null,
239
+ icon: null,
240
+ id: 'board-2',
241
+ name: 'External Roadmap',
242
+ ticket_prefix: null,
243
+ workspace: {
244
+ avatar_url: null,
245
+ id: 'ws-2',
246
+ logo_url: null,
247
+ name: 'Other Workspace',
248
+ personal: false,
249
+ },
250
+ ws_id: 'ws-2',
251
+ },
252
+ {
253
+ access_type: 'member',
254
+ archived_at: null,
255
+ created_at: '2026-06-01T00:00:00.000Z',
256
+ deleted_at: null,
257
+ icon: null,
258
+ id: 'board-current-secondary',
259
+ name: 'Current Roadmap',
260
+ ticket_prefix: null,
261
+ workspace: {
262
+ avatar_url: null,
263
+ id: 'ws-1',
264
+ logo_url: null,
265
+ name: 'Current Workspace',
266
+ personal: false,
267
+ },
268
+ ws_id: 'ws-1',
269
+ },
270
+ ],
271
+ });
272
+
273
+ renderBoardSwitcher();
274
+
275
+ await waitFor(() => {
276
+ expect(comboboxProps?.options.map((option) => option.value)).toEqual([
277
+ 'board-1',
278
+ 'board-current-secondary',
279
+ 'board-2',
280
+ ]);
281
+ });
282
+ expect(comboboxProps?.options[2]).toMatchObject({
283
+ group: 'Other Workspace',
284
+ label: 'External Roadmap',
285
+ value: 'board-2',
286
+ });
287
+
288
+ fireEvent.click(screen.getByTestId('board-combobox'));
289
+
290
+ expect(pushMock).toHaveBeenCalledWith('/ws-2/tasks/boards/board-2');
291
+ expect(updateUserWorkspaceConfigMock).not.toHaveBeenCalled();
292
+ });
293
+
208
294
  it('navigates without updating the default board when board memory is disabled', async () => {
209
295
  rememberLastBoardConfig.value = 'false';
210
296
  renderBoardSwitcher();
@@ -219,6 +305,41 @@ describe('BoardSwitcher', () => {
219
305
  expect(updateUserWorkspaceConfigMock).not.toHaveBeenCalled();
220
306
  });
221
307
 
308
+ it('hides board creation for guest-only board access', async () => {
309
+ listCurrentUserTaskBoardsMock.mockResolvedValue({
310
+ boards: [
311
+ {
312
+ access_type: 'guest',
313
+ archived_at: null,
314
+ created_at: '2026-06-01T00:00:00.000Z',
315
+ deleted_at: null,
316
+ guest_permission: 'edit',
317
+ icon: null,
318
+ id: 'board-1',
319
+ name: 'Tasks',
320
+ ticket_prefix: null,
321
+ workspace: {
322
+ avatar_url: null,
323
+ guest_products: ['tasks'],
324
+ id: 'ws-1',
325
+ logo_url: null,
326
+ name: 'Shared Workspace',
327
+ personal: false,
328
+ },
329
+ ws_id: 'ws-1',
330
+ },
331
+ ],
332
+ });
333
+
334
+ renderBoardSwitcher();
335
+
336
+ await waitFor(() => {
337
+ expect(comboboxProps?.createText).toBeUndefined();
338
+ expect(comboboxProps?.creatingText).toBeUndefined();
339
+ expect(comboboxProps?.onCreate).toBeUndefined();
340
+ });
341
+ });
342
+
222
343
  it('creates a new board from the picker and opens it', async () => {
223
344
  createWorkspaceTaskBoardMock.mockResolvedValue({
224
345
  board: {
@@ -33,6 +33,11 @@ let kanbanBoardProps:
33
33
  let listViewProps:
34
34
  | React.ComponentProps<typeof import('../list-view')['ListView']>
35
35
  | undefined;
36
+ let timelineBoardProps:
37
+ | React.ComponentProps<
38
+ typeof import('../../boards/boardId/timeline-board')['TimelineBoard']
39
+ >
40
+ | undefined;
36
41
 
37
42
  vi.mock('next-intl', () => ({
38
43
  useTranslations: () => (key: string) => key,
@@ -129,7 +134,10 @@ vi.mock('../list-view', () => ({
129
134
  }));
130
135
 
131
136
  vi.mock('../../boards/boardId/timeline-board', () => ({
132
- TimelineBoard: () => <div data-testid="timeline-view">Timeline</div>,
137
+ TimelineBoard: (props: any) => {
138
+ timelineBoardProps = props;
139
+ return <div data-testid="timeline-view">Timeline</div>;
140
+ },
133
141
  }));
134
142
 
135
143
  const mockBoard = {
@@ -246,6 +254,7 @@ describe('BoardViews', () => {
246
254
  boardHeaderProps = undefined;
247
255
  kanbanBoardProps = undefined;
248
256
  listViewProps = undefined;
257
+ timelineBoardProps = undefined;
249
258
  createTaskMock.mockReset();
250
259
  loadListPageMock.mockReset();
251
260
  progressivePagination = {};
@@ -305,6 +314,61 @@ describe('BoardViews', () => {
305
314
  expect(listWorkspaceTasksMock).not.toHaveBeenCalled();
306
315
  });
307
316
 
317
+ it('enables assignees for personal boards that have guest access', async () => {
318
+ renderBoardViews({
319
+ board: {
320
+ ...mockBoard,
321
+ has_guest_access: true,
322
+ },
323
+ workspace: { id: 'ws-1', personal: true },
324
+ });
325
+
326
+ expect(kanbanBoardProps?.canUseBoardAssignees).toBe(true);
327
+ expect(kanbanBoardProps?.assigneeMemberSource).toBe('board');
328
+
329
+ await act(async () => {
330
+ boardHeaderProps?.onViewChange('list');
331
+ });
332
+
333
+ await waitFor(() => {
334
+ expect(screen.getByTestId('list-view')).toBeInTheDocument();
335
+ });
336
+ expect(listViewProps?.canUseBoardAssignees).toBe(true);
337
+ expect(listViewProps?.assigneeMemberSource).toBe('board');
338
+
339
+ await act(async () => {
340
+ boardHeaderProps?.onViewChange('timeline');
341
+ });
342
+
343
+ await waitFor(() => {
344
+ expect(screen.getByTestId('timeline-view')).toBeInTheDocument();
345
+ });
346
+ expect(timelineBoardProps?.canUseBoardAssignees).toBe(true);
347
+ expect(timelineBoardProps?.assigneeMemberSource).toBe('board');
348
+ });
349
+
350
+ it('keeps assignees hidden for unshared personal boards', () => {
351
+ renderBoardViews({
352
+ workspace: { id: 'ws-1', personal: true },
353
+ });
354
+
355
+ expect(kanbanBoardProps?.canUseBoardAssignees).toBe(false);
356
+ expect(kanbanBoardProps?.assigneeMemberSource).toBe('workspace');
357
+ });
358
+
359
+ it('merges workspace and board assignee sources for team boards that have guest access', () => {
360
+ renderBoardViews({
361
+ board: {
362
+ ...mockBoard,
363
+ has_guest_access: true,
364
+ },
365
+ workspace: { id: 'ws-1', personal: false },
366
+ });
367
+
368
+ expect(kanbanBoardProps?.canUseBoardAssignees).toBe(true);
369
+ expect(kanbanBoardProps?.assigneeMemberSource).toBe('workspace-and-board');
370
+ });
371
+
308
372
  it('renders board-scoped drafts and recycle bin views from the header mode switcher', async () => {
309
373
  renderBoardViews();
310
374
 
@@ -526,7 +590,7 @@ describe('BoardViews', () => {
526
590
  );
527
591
  });
528
592
 
529
- it('expands the virtual external task list by default when assigned external tasks exist', () => {
593
+ it('collapses the virtual external task list by default even when assigned external tasks exist', () => {
530
594
  renderBoardViews({
531
595
  workspace: {
532
596
  ...mockWorkspace,
@@ -549,7 +613,7 @@ describe('BoardViews', () => {
549
613
  expect(kanbanBoardProps?.lists[0]).toEqual(
550
614
  expect.objectContaining({
551
615
  id: 'personal-external-staging:board-1',
552
- is_external_collapsed: false,
616
+ is_external_collapsed: true,
553
617
  is_external_staging: true,
554
618
  })
555
619
  );
@@ -648,10 +712,10 @@ describe('BoardViews', () => {
648
712
  });
649
713
  });
650
714
 
651
- it('persists deadline section collapse state per board and section', async () => {
715
+ it('collapses deadline sections by default and persists per board and section', async () => {
652
716
  window.localStorage.setItem(
653
717
  'task-board-deadline-section-collapsed:board-1:overdue',
654
- 'true'
718
+ 'false'
655
719
  );
656
720
 
657
721
  renderBoardViews();
@@ -659,14 +723,14 @@ describe('BoardViews', () => {
659
723
  await waitFor(() => {
660
724
  expect(kanbanBoardProps?.deadlineSectionsCollapsed).toEqual(
661
725
  expect.objectContaining({
662
- overdue: true,
663
- upcoming: false,
726
+ overdue: false,
727
+ upcoming: true,
664
728
  })
665
729
  );
666
730
  });
667
731
 
668
732
  act(() => {
669
- kanbanBoardProps?.onDeadlineSectionCollapsedChange?.('upcoming', true);
733
+ kanbanBoardProps?.onDeadlineSectionCollapsedChange?.('upcoming', false);
670
734
  });
671
735
 
672
736
  await waitFor(() => {
@@ -674,13 +738,96 @@ describe('BoardViews', () => {
674
738
  window.localStorage.getItem(
675
739
  'task-board-deadline-section-collapsed:board-1:upcoming'
676
740
  )
677
- ).toBe('true');
741
+ ).toBe('false');
678
742
  expect(kanbanBoardProps?.deadlineSectionsCollapsed).toEqual(
679
743
  expect.objectContaining({
744
+ overdue: false,
745
+ upcoming: false,
746
+ })
747
+ );
748
+ });
749
+ });
750
+
751
+ it('does not force pinned special task lists to stay expanded', async () => {
752
+ getUserWorkspaceConfigMock.mockImplementation(
753
+ (_workspaceId: string, configId: string) =>
754
+ Promise.resolve({
755
+ value:
756
+ configId === 'TASK_BOARD_PINNED_SPECIAL_LISTS'
757
+ ? JSON.stringify([
758
+ 'external_tasks',
759
+ 'closed_tasks',
760
+ 'overdue',
761
+ 'upcoming',
762
+ ])
763
+ : null,
764
+ })
765
+ );
766
+
767
+ renderBoardViews({
768
+ lists: [...mockLists, closedList],
769
+ workspace: {
770
+ ...mockWorkspace,
771
+ personal: true,
772
+ },
773
+ });
774
+
775
+ await waitFor(() => {
776
+ expect(kanbanBoardProps?.specialTaskListPins).toEqual(
777
+ expect.objectContaining({
778
+ closed_tasks: true,
779
+ external_tasks: true,
680
780
  overdue: true,
681
781
  upcoming: true,
682
782
  })
683
783
  );
784
+ expect(kanbanBoardProps?.lists[0]).toEqual(
785
+ expect.objectContaining({
786
+ is_external_collapsed: true,
787
+ is_external_staging: true,
788
+ })
789
+ );
790
+ expect(
791
+ kanbanBoardProps?.lists.find((list) => list.id === 'list-closed')
792
+ ).toEqual(
793
+ expect.objectContaining({
794
+ is_collapsed: true,
795
+ status: 'closed',
796
+ })
797
+ );
798
+ expect(kanbanBoardProps?.deadlineSectionsCollapsed).toEqual(
799
+ expect.objectContaining({
800
+ overdue: true,
801
+ upcoming: true,
802
+ })
803
+ );
804
+ });
805
+
806
+ act(() => {
807
+ kanbanBoardProps?.onExternalTasksCollapsedChange?.(false);
808
+ kanbanBoardProps?.onTaskListCollapsedChange?.('list-closed', false);
809
+ kanbanBoardProps?.onDeadlineSectionCollapsedChange?.('overdue', false);
810
+ });
811
+
812
+ await waitFor(() => {
813
+ expect(kanbanBoardProps?.lists[0]).toEqual(
814
+ expect.objectContaining({
815
+ is_external_collapsed: false,
816
+ })
817
+ );
818
+ expect(
819
+ kanbanBoardProps?.lists.find((list) => list.id === 'list-closed')
820
+ ).toEqual(
821
+ expect.objectContaining({
822
+ is_collapsed: false,
823
+ })
824
+ );
825
+ expect(kanbanBoardProps?.deadlineSectionsCollapsed).toEqual(
826
+ expect.objectContaining({
827
+ overdue: false,
828
+ upcoming: true,
829
+ })
830
+ );
684
831
  });
685
832
  });
686
833
 
@@ -987,6 +1134,72 @@ describe('BoardViews', () => {
987
1134
  });
988
1135
  });
989
1136
 
1137
+ it('uses the route default view instead of stale local My Tasks config', async () => {
1138
+ window.localStorage.setItem(
1139
+ getBoardConfigKey(mockBoard.id),
1140
+ JSON.stringify({
1141
+ currentView: 'my_tasks',
1142
+ filters: {
1143
+ assignees: [],
1144
+ dueDateRange: null,
1145
+ estimationRange: null,
1146
+ includeMyTasks: false,
1147
+ includeUnassigned: false,
1148
+ labels: [],
1149
+ priorities: [],
1150
+ projects: [],
1151
+ sourceBoardIds: [],
1152
+ sourceScope: 'all_visible',
1153
+ sourceWorkspaceIds: [],
1154
+ },
1155
+ listStatusFilter: 'all',
1156
+ })
1157
+ );
1158
+ window.history.replaceState({}, '', '/ws-1/tasks/boards/board-1');
1159
+
1160
+ renderBoardViews({ props: { defaultView: 'kanban' } });
1161
+
1162
+ await waitFor(() => {
1163
+ expect(screen.getByTestId('kanban-view')).toBeInTheDocument();
1164
+ });
1165
+ expect(screen.queryByTestId('my-tasks-view')).not.toBeInTheDocument();
1166
+ });
1167
+
1168
+ it('lets an explicit URL view override the route default view', async () => {
1169
+ window.localStorage.setItem(
1170
+ getBoardConfigKey(mockBoard.id),
1171
+ JSON.stringify({
1172
+ currentView: 'timeline',
1173
+ filters: {
1174
+ assignees: [],
1175
+ dueDateRange: null,
1176
+ estimationRange: null,
1177
+ includeMyTasks: false,
1178
+ includeUnassigned: false,
1179
+ labels: [],
1180
+ priorities: [],
1181
+ projects: [],
1182
+ sourceBoardIds: [],
1183
+ sourceScope: 'all_visible',
1184
+ sourceWorkspaceIds: [],
1185
+ },
1186
+ listStatusFilter: 'all',
1187
+ })
1188
+ );
1189
+ window.history.replaceState(
1190
+ {},
1191
+ '',
1192
+ '/ws-1/tasks/boards/board-1?view=my_tasks'
1193
+ );
1194
+
1195
+ renderBoardViews({ props: { defaultView: 'kanban' } });
1196
+
1197
+ await waitFor(() => {
1198
+ expect(screen.getByTestId('my-tasks-view')).toBeInTheDocument();
1199
+ });
1200
+ expect(screen.queryByTestId('kanban-view')).not.toBeInTheDocument();
1201
+ });
1202
+
990
1203
  it('ignores board hotkeys while typing in an input', async () => {
991
1204
  renderBoardViews();
992
1205
  const input = screen.getByTestId('board-header-input');
@@ -10,8 +10,29 @@ describe('TaskBoardLoadingState', () => {
10
10
  expect(screen.getByTestId('task-board-loading-state')).toHaveClass(
11
11
  '-m-4',
12
12
  'h-[calc(100dvh+2rem)]',
13
+ 'w-[calc(100%+2rem)]',
13
14
  'bg-transparent'
14
15
  );
15
16
  expect(screen.getByTestId('kanban-skeleton')).toHaveClass('bg-transparent');
17
+ expect(screen.getByTestId('kanban-skeleton-frame')).toHaveClass(
18
+ 'py-2',
19
+ 'pl-2',
20
+ 'pr-0'
21
+ );
22
+ expect(screen.getByTestId('kanban-skeleton-frame')).not.toHaveClass('p-2');
23
+ });
24
+
25
+ it('keeps embedded loading skeletons constrained to the parent width', () => {
26
+ render(<TaskBoardLoadingState />);
27
+
28
+ expect(screen.getByTestId('task-board-loading-state')).toHaveClass(
29
+ 'w-full',
30
+ 'h-[calc(100dvh-1rem)]'
31
+ );
32
+ expect(screen.getByTestId('task-board-loading-state')).not.toHaveClass(
33
+ '-m-4',
34
+ 'w-[calc(100%+2rem)]'
35
+ );
36
+ expect(screen.getByTestId('kanban-skeleton-frame')).toHaveClass('p-2');
16
37
  });
17
38
  });