@tuturuuu/ui 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +82 -70
  3. package/src/components/ui/__tests__/avatar.test.tsx +8 -5
  4. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
  5. package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
  6. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
  8. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
  9. package/src/components/ui/chart.test.tsx +29 -0
  10. package/src/components/ui/chart.tsx +12 -3
  11. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +43 -13
  12. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
  13. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
  14. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
  15. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
  16. package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
  17. package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -3
  18. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  19. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  20. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  21. package/src/components/ui/custom/common-footer.tsx +16 -1
  22. package/src/components/ui/custom/production-indicator.tsx +1 -1
  23. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  24. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  25. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  26. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  27. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  28. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  29. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  30. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  31. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  32. package/src/components/ui/custom/workspace-select.tsx +33 -12
  33. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  34. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  35. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  36. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  37. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  38. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  39. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  40. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  41. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  42. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  43. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  44. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  45. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  46. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  47. package/src/components/ui/finance/invoices/utils.ts +75 -17
  48. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  49. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  50. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  51. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  52. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  53. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  54. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  55. package/src/components/ui/finance/transactions/form.tsx +60 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  57. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  58. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  59. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  60. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  61. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  62. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  63. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  64. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  65. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  66. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  67. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  68. package/src/components/ui/legacy/meet/page.tsx +87 -39
  69. package/src/components/ui/legacy/meet/planId/page.tsx +10 -4
  70. package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
  71. package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
  72. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  73. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  74. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  75. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  77. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  78. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  79. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  80. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  81. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  82. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  83. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  84. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  85. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  86. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  87. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  88. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  89. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  90. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  91. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  92. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  93. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  94. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  95. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  96. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  97. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  98. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  99. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  100. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  101. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  102. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  103. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  104. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  105. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  106. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  107. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  108. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  109. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  110. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  111. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  112. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  113. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-realtime-sync.test.tsx +37 -9
  114. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  115. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +89 -70
  116. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
  117. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
  118. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
  119. package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
  120. package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
  121. package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
  122. package/src/hooks/use-calendar-sync.tsx +22 -277
  123. package/src/hooks/use-calendar.tsx +95 -525
  124. package/src/hooks/use-task-actions.ts +43 -117
  125. package/src/hooks/use-user-config.ts +1 -1
  126. package/src/hooks/use-workspace-config.ts +6 -2
  127. package/src/hooks/use-workspace-presence.ts +1 -1
  128. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -102,15 +102,20 @@ vi.mock('../../sonner', () => ({
102
102
  },
103
103
  }));
104
104
 
105
- function renderWithQueryClient(children: ReactNode) {
106
- const queryClient = new QueryClient({
105
+ function createTestQueryClient() {
106
+ return new QueryClient({
107
107
  defaultOptions: {
108
108
  queries: {
109
109
  retry: false,
110
110
  },
111
111
  },
112
112
  });
113
+ }
113
114
 
115
+ function renderWithQueryClient(
116
+ children: ReactNode,
117
+ queryClient = createTestQueryClient()
118
+ ) {
114
119
  return render(
115
120
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
116
121
  );
@@ -217,7 +222,112 @@ describe('TaskMentionChip', () => {
217
222
  expect(screen.getByText('Complete Team Charter V1.0')).toBeInTheDocument();
218
223
  });
219
224
 
220
- it('uses the mention workspace when repairing a task from another workspace', async () => {
225
+ it('does not reuse stale mention fallback cache across board contexts', async () => {
226
+ const boardOneTask = {
227
+ assignees: [],
228
+ board_id: 'board-1',
229
+ display_number: 3,
230
+ id: 'board-one-task-id',
231
+ labels: [],
232
+ list_id: 'list-1',
233
+ name: 'Board one task',
234
+ projects: [],
235
+ ticket_prefix: null,
236
+ };
237
+ const boardTwoTask = {
238
+ assignees: [],
239
+ board_id: 'board-2',
240
+ display_number: 4,
241
+ id: 'board-two-task-id',
242
+ labels: [],
243
+ list_id: 'list-2',
244
+ name: 'Board two task',
245
+ projects: [],
246
+ ticket_prefix: null,
247
+ };
248
+ const queryClient = createTestQueryClient();
249
+ const firstResolved = vi.fn();
250
+ const secondResolved = vi.fn();
251
+
252
+ mocks.getCurrentUserTask.mockImplementation(async (taskId: string) => {
253
+ if (taskId === 'stale-task-id') {
254
+ throw new Error('Task not found');
255
+ }
256
+
257
+ const task =
258
+ taskId === boardOneTask.id
259
+ ? boardOneTask
260
+ : taskId === boardTwoTask.id
261
+ ? boardTwoTask
262
+ : null;
263
+
264
+ if (!task) {
265
+ throw new Error('Task not found');
266
+ }
267
+
268
+ return {
269
+ availableLists: [],
270
+ task,
271
+ taskWorkspacePersonal: false,
272
+ taskWorkspaceTier: 'FREE',
273
+ taskWsId: 'ws-1',
274
+ };
275
+ });
276
+ mocks.listWorkspaceTasks.mockImplementation(
277
+ async (_wsId: string, params: { boardId?: string }) => ({
278
+ tasks: params.boardId === 'board-2' ? [boardTwoTask] : [boardOneTask],
279
+ })
280
+ );
281
+
282
+ const firstRender = renderWithQueryClient(
283
+ <TaskMentionChip
284
+ entityId="stale-task-id"
285
+ displayNumber="3"
286
+ subtitle="Board one task"
287
+ onResolvedTaskMention={firstResolved}
288
+ />,
289
+ queryClient
290
+ );
291
+
292
+ await waitFor(() => {
293
+ expect(firstResolved).toHaveBeenCalledWith(
294
+ expect.objectContaining({ entityId: boardOneTask.id })
295
+ );
296
+ });
297
+
298
+ firstRender.unmount();
299
+ window.history.pushState({}, '', '/ws-1/tasks/boards/board-2');
300
+
301
+ renderWithQueryClient(
302
+ <TaskMentionChip
303
+ entityId="stale-task-id"
304
+ displayNumber="4"
305
+ subtitle="Board two task"
306
+ onResolvedTaskMention={secondResolved}
307
+ />,
308
+ queryClient
309
+ );
310
+
311
+ await waitFor(() => {
312
+ expect(mocks.listWorkspaceTasks).toHaveBeenCalledWith(
313
+ 'ws-1',
314
+ expect.objectContaining({
315
+ boardId: 'board-2',
316
+ identifier: '4',
317
+ })
318
+ );
319
+ });
320
+ await waitFor(() => {
321
+ expect(secondResolved).toHaveBeenCalledWith(
322
+ expect.objectContaining({ entityId: boardTwoTask.id })
323
+ );
324
+ });
325
+ expect(secondResolved).not.toHaveBeenCalledWith(
326
+ expect.objectContaining({ entityId: boardOneTask.id })
327
+ );
328
+ });
329
+
330
+ it('ignores untrusted mention workspace when repairing stale task mentions', async () => {
221
331
  const resolvedTask = {
222
332
  assignees: [],
223
333
  board_id: 'board-1',
@@ -242,7 +352,7 @@ describe('TaskMentionChip', () => {
242
352
  task: resolvedTask,
243
353
  taskWorkspacePersonal: false,
244
354
  taskWorkspaceTier: 'FREE',
245
- taskWsId: 'source-ws',
355
+ taskWsId: 'route-ws',
246
356
  };
247
357
  });
248
358
  mocks.listWorkspaceTasks.mockResolvedValue({ tasks: [resolvedTask] });
@@ -259,7 +369,7 @@ describe('TaskMentionChip', () => {
259
369
 
260
370
  await waitFor(() => {
261
371
  expect(mocks.listWorkspaceTasks).toHaveBeenCalledWith(
262
- 'source-ws',
372
+ 'route-ws',
263
373
  expect.objectContaining({
264
374
  boardId: 'board-1',
265
375
  identifier: '7',
@@ -271,9 +381,96 @@ describe('TaskMentionChip', () => {
271
381
  expect(onResolvedTaskMention).toHaveBeenCalledWith(
272
382
  expect.objectContaining({
273
383
  entityId: 'source-task-id',
274
- workspaceId: 'source-ws',
384
+ workspaceId: 'route-ws',
275
385
  })
276
386
  );
277
387
  });
278
388
  });
389
+
390
+ it('does not rewrite stale mentions to tasks from another workspace', async () => {
391
+ const resolvedTask = {
392
+ assignees: [],
393
+ board_id: 'board-1',
394
+ display_number: 8,
395
+ id: 'source-task-id',
396
+ labels: [],
397
+ list_id: 'list-1',
398
+ name: 'Source workspace task',
399
+ projects: [],
400
+ ticket_prefix: null,
401
+ };
402
+ const onResolvedTaskMention = vi.fn();
403
+
404
+ window.history.pushState({}, '', '/route-ws/tasks/boards/board-1');
405
+ mocks.getCurrentUserTask.mockImplementation(async (taskId: string) => {
406
+ if (taskId === 'stale-task-id') {
407
+ throw new Error('Task not found');
408
+ }
409
+
410
+ return {
411
+ availableLists: [],
412
+ task: resolvedTask,
413
+ taskWorkspacePersonal: false,
414
+ taskWorkspaceTier: 'FREE',
415
+ taskWsId: 'source-ws',
416
+ };
417
+ });
418
+ mocks.listWorkspaceTasks.mockResolvedValue({ tasks: [resolvedTask] });
419
+
420
+ renderWithQueryClient(
421
+ <TaskMentionChip
422
+ entityId="stale-task-id"
423
+ displayNumber="8"
424
+ subtitle="Source workspace task"
425
+ onResolvedTaskMention={onResolvedTaskMention}
426
+ />
427
+ );
428
+
429
+ await waitFor(() => {
430
+ expect(mocks.getCurrentUserTask).toHaveBeenCalledWith('source-task-id');
431
+ });
432
+
433
+ expect(onResolvedTaskMention).not.toHaveBeenCalled();
434
+ });
435
+
436
+ it('uses the server-returned workspace for resolved cross-workspace task mentions', async () => {
437
+ const resolvedTask = {
438
+ assignees: [],
439
+ board_id: 'source-board',
440
+ display_number: 9,
441
+ id: 'source-task-id',
442
+ labels: [],
443
+ list_id: 'list-1',
444
+ name: 'Trusted cross workspace task',
445
+ projects: [],
446
+ ticket_prefix: null,
447
+ };
448
+
449
+ window.history.pushState({}, '', '/route-ws/tasks/boards/board-1');
450
+ mocks.getCurrentUserTask.mockResolvedValue({
451
+ availableLists: [],
452
+ task: resolvedTask,
453
+ taskWorkspacePersonal: false,
454
+ taskWorkspaceTier: 'FREE',
455
+ taskWsId: 'source-ws',
456
+ });
457
+
458
+ renderWithQueryClient(
459
+ <TaskMentionChip
460
+ entityId="source-task-id"
461
+ displayNumber="9"
462
+ subtitle="Trusted cross workspace task"
463
+ workspaceId="source-ws"
464
+ />
465
+ );
466
+
467
+ await waitFor(() => {
468
+ expect(mocks.getCurrentUserTask).toHaveBeenCalledWith('source-task-id');
469
+ });
470
+
471
+ expect(mocks.listWorkspaceTasks).not.toHaveBeenCalled();
472
+ expect(
473
+ screen.getByText('Trusted cross workspace task')
474
+ ).toBeInTheDocument();
475
+ });
279
476
  });
@@ -186,7 +186,6 @@ export function TaskMentionChip({
186
186
  displayNumber,
187
187
  avatarUrl,
188
188
  subtitle,
189
- workspaceId,
190
189
  className,
191
190
  editor: editorProp,
192
191
  onResolvedTaskMention,
@@ -213,8 +212,19 @@ export function TaskMentionChip({
213
212
 
214
213
  return getRouteTaskBoardIdFromPathname(window.location.pathname);
215
214
  }, []);
216
- const mentionWorkspaceId = workspaceId?.trim() || undefined;
217
- const resolutionWorkspaceId = mentionWorkspaceId ?? routeWsId;
215
+ const resolutionWorkspaceId = routeWsId;
216
+ const taskMentionQueryKey = useMemo(
217
+ () => [
218
+ 'task',
219
+ 'mention',
220
+ entityId,
221
+ resolutionWorkspaceId ?? null,
222
+ routeBoardId ?? null,
223
+ displayNumber.trim(),
224
+ subtitle?.trim() ?? null,
225
+ ],
226
+ [displayNumber, entityId, resolutionWorkspaceId, routeBoardId, subtitle]
227
+ );
218
228
  const isDark = useDomResolvedTheme() === 'dark';
219
229
 
220
230
  // Dialog states
@@ -230,7 +240,7 @@ export function TaskMentionChip({
230
240
  isLoading: taskLoading,
231
241
  error: taskError,
232
242
  } = useQuery({
233
- queryKey: ['task', entityId, resolutionWorkspaceId],
243
+ queryKey: taskMentionQueryKey,
234
244
  queryFn: async () => {
235
245
  return resolveTaskMentionPayload({
236
246
  displayNumber,
@@ -249,9 +259,19 @@ export function TaskMentionChip({
249
259
  const resolvedTaskId = task?.id ?? entityId;
250
260
  const taskWorkspaceId = taskPayload?.taskWsId;
251
261
  const taskWorkspacePersonal = taskPayload?.taskWorkspacePersonal;
252
- const canonicalWorkspaceId =
253
- mentionWorkspaceId ?? taskWorkspaceId ?? routeWsId;
262
+ const canonicalWorkspaceId = taskWorkspaceId ?? routeWsId;
254
263
  const boardWorkspaceId = canonicalWorkspaceId;
264
+ const resolvedTaskBelongsToRouteWorkspace = useMemo(() => {
265
+ if (!routeWsId || !taskWorkspaceId) {
266
+ return false;
267
+ }
268
+
269
+ if (taskWorkspaceId === routeWsId) {
270
+ return true;
271
+ }
272
+
273
+ return routeWsId === 'personal' && taskWorkspacePersonal === true;
274
+ }, [routeWsId, taskWorkspaceId, taskWorkspacePersonal]);
255
275
 
256
276
  // Get board config - only fetch when menu opens and we have task data
257
277
  const { data: boardConfig } = useBoardConfig(
@@ -394,6 +414,7 @@ export function TaskMentionChip({
394
414
  }, [availableLists, task]);
395
415
 
396
416
  const handleUpdate = useCallback(() => {
417
+ queryClient.invalidateQueries({ queryKey: ['task', 'mention'] });
397
418
  queryClient.invalidateQueries({ queryKey: ['task', entityId] });
398
419
  if (resolvedTaskId !== entityId) {
399
420
  queryClient.invalidateQueries({ queryKey: ['task', resolvedTaskId] });
@@ -668,7 +689,7 @@ export function TaskMentionChip({
668
689
  }, [task?.name, subtitle]);
669
690
 
670
691
  useEffect(() => {
671
- if (!task || task.id === entityId) {
692
+ if (!task || task.id === entityId || !resolvedTaskBelongsToRouteWorkspace) {
672
693
  return;
673
694
  }
674
695
 
@@ -697,6 +718,7 @@ export function TaskMentionChip({
697
718
  entityId,
698
719
  onResolvedTaskMention,
699
720
  queryClient,
721
+ resolvedTaskBelongsToRouteWorkspace,
700
722
  subtitle,
701
723
  task,
702
724
  taskPayload,
@@ -40,8 +40,13 @@ import {
40
40
  type ListPaginationState,
41
41
  useProgressiveLoader,
42
42
  } from '../../shared/progressive-loader-context';
43
+ import { getListTextColorClass } from '../../utils/taskColorUtils';
43
44
  import { normalizeBoardText } from './board-text-utils';
44
45
  import type { DragPreviewPosition } from './kanban/dnd/use-kanban-dnd';
46
+ import {
47
+ isClosedTaskListColumnCollapsed,
48
+ isKanbanColumnCollapsed,
49
+ } from './kanban/kanban-column-collapse';
45
50
  import { ListActions } from './list-actions';
46
51
  import { statusIcons } from './status-section';
47
52
  import type { TaskFilters } from './task-filter';
@@ -168,6 +173,7 @@ interface BoardColumnProps {
168
173
  workspaceId?: string;
169
174
  wsId: string;
170
175
  onExternalTasksCollapsedChange?: (collapsed: boolean) => void;
176
+ onTaskListCollapsedChange?: (listId: string, collapsed: boolean) => void;
171
177
  }
172
178
 
173
179
  export function BoardColumn({
@@ -193,6 +199,7 @@ export function BoardColumn({
193
199
  workspaceId,
194
200
  wsId,
195
201
  onExternalTasksCollapsedChange,
202
+ onTaskListCollapsedChange,
196
203
  }: BoardColumnProps) {
197
204
  const t = useTranslations('common');
198
205
  const tTasks = useTranslations('ws-tasks');
@@ -211,6 +218,8 @@ export function BoardColumn({
211
218
  const [externalSortBy, setExternalSortBy] = useState<ExternalTaskSortBy>(
212
219
  DEFAULT_EXTERNAL_TASK_SORT_BY
213
220
  );
221
+ const isClosedCollapsed = isClosedTaskListColumnCollapsed(column);
222
+ const isColumnCollapsed = isKanbanColumnCollapsed(column);
214
223
  const hasActiveFilters =
215
224
  !!filters &&
216
225
  (filters.labels.length > 0 ||
@@ -291,7 +300,7 @@ export function BoardColumn({
291
300
  useEffect(() => {
292
301
  if (
293
302
  !isExternalStaging ||
294
- isExternalCollapsed ||
303
+ isColumnCollapsed ||
295
304
  !listState ||
296
305
  listState.isInitialLoad ||
297
306
  listState.isLoading ||
@@ -303,7 +312,7 @@ export function BoardColumn({
303
312
  loadColumnPage(0);
304
313
  }, [
305
314
  externalOptionsSignature,
306
- isExternalCollapsed,
315
+ isColumnCollapsed,
307
316
  isExternalStaging,
308
317
  listState,
309
318
  listState?.isInitialLoad,
@@ -315,7 +324,7 @@ export function BoardColumn({
315
324
  // cache was cleared, refetch page 0 for this list so cards reappear.
316
325
  useEffect(() => {
317
326
  if (
318
- isExternalCollapsed ||
327
+ isColumnCollapsed ||
319
328
  !listState ||
320
329
  listState.isLoading ||
321
330
  hasActiveFilters
@@ -334,7 +343,7 @@ export function BoardColumn({
334
343
  recoveryRequestedRef.current = false;
335
344
  }, [
336
345
  hasActiveFilters,
337
- isExternalCollapsed,
346
+ isColumnCollapsed,
338
347
  listState,
339
348
  listState?.isLoading,
340
349
  listState?.totalCount,
@@ -481,22 +490,45 @@ export function BoardColumn({
481
490
  });
482
491
  };
483
492
 
484
- if (isExternalCollapsed) {
493
+ if (isColumnCollapsed) {
494
+ const collapsedListName = translateListName(column.name);
495
+ const expandLabel = isExternalCollapsed
496
+ ? tTasks('expand_external_tasks')
497
+ : tTasks('expand_task_list', { name: collapsedListName });
498
+ const collapsedTextColor = isExternalCollapsed
499
+ ? 'text-dynamic-cyan'
500
+ : getListTextColorClass(column.color as SupportedColor);
501
+
485
502
  return (
486
503
  <Card
487
504
  ref={composedRef}
488
505
  style={style}
489
506
  className={cn(
490
- 'group flex h-full w-14 shrink-0 snap-start flex-col items-center rounded-xl border border-dynamic-cyan/45 border-dashed bg-dynamic-cyan/[0.035] transition-all duration-200',
491
- 'touch-none select-none overflow-hidden hover:shadow-md'
507
+ 'group flex h-full w-14 shrink-0 snap-start flex-col items-center rounded-xl border border-dashed transition-all duration-200',
508
+ 'touch-none select-none overflow-hidden hover:shadow-md',
509
+ isExternalCollapsed
510
+ ? 'border-dynamic-cyan/45 bg-dynamic-cyan/[0.035]'
511
+ : colorClass
492
512
  )}
493
513
  >
494
514
  <button
495
515
  type="button"
496
- className="flex h-full w-full flex-col items-center gap-3 rounded-xl px-1 py-3 text-dynamic-cyan transition-colors hover:bg-dynamic-cyan/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-dynamic-cyan/40"
497
- title={tTasks('expand_external_tasks')}
498
- aria-label={tTasks('expand_external_tasks')}
499
- onClick={() => onExternalTasksCollapsedChange?.(false)}
516
+ className={cn(
517
+ 'flex h-full w-full flex-col items-center gap-3 rounded-xl px-1 py-3 transition-colors focus-visible:outline-none focus-visible:ring-2',
518
+ isExternalCollapsed
519
+ ? 'text-dynamic-cyan hover:bg-dynamic-cyan/10 focus-visible:ring-dynamic-cyan/40'
520
+ : `${collapsedTextColor} hover:bg-muted/40 focus-visible:ring-primary/40`
521
+ )}
522
+ title={expandLabel}
523
+ aria-label={expandLabel}
524
+ onClick={() => {
525
+ if (isExternalCollapsed) {
526
+ onExternalTasksCollapsedChange?.(false);
527
+ return;
528
+ }
529
+
530
+ onTaskListCollapsedChange?.(column.id, false);
531
+ }}
500
532
  >
501
533
  <ChevronRight className="h-4 w-4 shrink-0" />
502
534
  <Badge
@@ -509,7 +541,7 @@ export function BoardColumn({
509
541
  className="max-h-48 truncate font-medium text-[11px]"
510
542
  style={{ writingMode: 'vertical-rl' }}
511
543
  >
512
- {translateListName(column.name)}
544
+ {collapsedListName}
513
545
  </span>
514
546
  </button>
515
547
  </Card>
@@ -688,19 +720,41 @@ export function BoardColumn({
688
720
  </Button>
689
721
  </>
690
722
  ) : (
691
- <ListActions
692
- listId={column.id}
693
- listName={column.name}
694
- listStatus={column.status}
695
- listColor={column.color as SupportedColor}
696
- tasks={tasks}
697
- boardId={boardId}
698
- wsId={wsId}
699
- onUpdate={handleUpdate}
700
- onSelectAll={handleSelectAll}
701
- isEditOpen={isEditOpen}
702
- onEditOpenChange={setIsEditOpen}
703
- />
723
+ <>
724
+ {isClosedCollapsed || column.status === 'closed' ? (
725
+ <Button
726
+ type="button"
727
+ variant="ghost"
728
+ size="xs"
729
+ className={cn(
730
+ 'h-7 w-7 p-0 hover:bg-muted/40',
731
+ getListTextColorClass(column.color as SupportedColor)
732
+ )}
733
+ title={tTasks('collapse_task_list', {
734
+ name: translateListName(column.name),
735
+ })}
736
+ aria-label={tTasks('collapse_task_list', {
737
+ name: translateListName(column.name),
738
+ })}
739
+ onClick={() => onTaskListCollapsedChange?.(column.id, true)}
740
+ >
741
+ <ChevronLeft className="h-3.5 w-3.5" />
742
+ </Button>
743
+ ) : null}
744
+ <ListActions
745
+ listId={column.id}
746
+ listName={column.name}
747
+ listStatus={column.status}
748
+ listColor={column.color as SupportedColor}
749
+ tasks={tasks}
750
+ boardId={boardId}
751
+ wsId={wsId}
752
+ onUpdate={handleUpdate}
753
+ onSelectAll={handleSelectAll}
754
+ isEditOpen={isEditOpen}
755
+ onEditOpenChange={setIsEditOpen}
756
+ />
757
+ </>
704
758
  )}
705
759
  </div>
706
760
  </div>