@tuturuuu/ui 0.8.0 → 0.9.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 (182) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/biome.json +1 -1
  3. package/package.json +73 -71
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  29. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  30. package/src/components/ui/custom/combobox.test.tsx +195 -0
  31. package/src/components/ui/custom/combobox.tsx +273 -156
  32. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  33. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  34. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  35. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  36. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  37. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  38. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  39. package/src/components/ui/custom/workspace-select.tsx +8 -3
  40. package/src/components/ui/dialog.test.tsx +52 -0
  41. package/src/components/ui/dialog.tsx +6 -2
  42. package/src/components/ui/dropdown-menu.tsx +5 -1
  43. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  44. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  45. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  46. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  47. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  48. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  49. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  50. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  51. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  52. package/src/components/ui/finance/invoices/utils.ts +3 -1
  53. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  54. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  55. package/src/components/ui/finance/transactions/form.tsx +2 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  57. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  58. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  59. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  60. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  61. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  62. package/src/components/ui/finance/wallets/form.tsx +15 -4
  63. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  64. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  65. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  66. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  67. package/src/components/ui/input-otp.tsx +1 -1
  68. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  69. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  70. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  71. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  72. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  73. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  74. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  75. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  76. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  77. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  78. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  79. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  80. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  81. package/src/components/ui/navigation-menu.tsx +1 -1
  82. package/src/components/ui/pagination.tsx +1 -1
  83. package/src/components/ui/radio-group.tsx +1 -1
  84. package/src/components/ui/select.tsx +5 -1
  85. package/src/components/ui/sheet.tsx +1 -1
  86. package/src/components/ui/sidebar.tsx +1 -1
  87. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  88. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  89. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  90. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  91. package/src/components/ui/storefront/listing-card.tsx +1 -1
  92. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  93. package/src/components/ui/storefront/product-detail.tsx +1 -1
  94. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  95. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  96. package/src/components/ui/storefront/types.ts +4 -0
  97. package/src/components/ui/storefront/utils.ts +6 -0
  98. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  99. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  100. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  101. package/src/components/ui/text-editor/editor.tsx +69 -14
  102. package/src/components/ui/text-editor/extensions.ts +8 -2
  103. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  104. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  105. package/src/components/ui/toast.tsx +1 -1
  106. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  107. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  108. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  109. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
  110. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  111. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  112. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  113. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  114. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  115. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  116. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  117. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  118. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  119. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  120. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  121. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  122. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  127. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  128. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  129. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  131. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
  132. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
  133. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  134. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  135. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  136. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
  137. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
  138. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  139. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  140. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  141. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  142. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  143. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  144. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  145. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  146. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  147. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  148. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  149. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  150. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  151. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  152. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  153. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  154. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
  155. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  156. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  157. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  158. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  159. package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
  160. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  161. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  162. package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
  163. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  164. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  165. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  166. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  167. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  168. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  169. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  170. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  171. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  172. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
  173. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  174. package/src/declarations.d.ts +1 -0
  175. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  176. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  177. package/src/hooks/use-calendar-sync.tsx +247 -243
  178. package/src/hooks/use-calendar.tsx +323 -138
  179. package/src/hooks/use-task-actions.ts +24 -0
  180. package/src/hooks/use-user-workspace-config.ts +75 -0
  181. package/src/hooks/use-workspace-currency.ts +8 -3
  182. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
@@ -1,42 +1,104 @@
1
1
  'use client';
2
2
 
3
- import { Card, CardContent } from '@tuturuuu/ui/card';
3
+ import { Skeleton } from '@tuturuuu/ui/skeleton';
4
+ import { cn } from '@tuturuuu/utils/format';
5
+
6
+ const RAILS = ['upcoming', 'external'];
7
+ const COLUMNS = [
8
+ { id: 'documents', cards: 3, width: 'w-[21rem] sm:w-[23rem]' },
9
+ { id: 'inactive', cards: 1, width: 'w-[20rem] sm:w-[22rem]' },
10
+ { id: 'todo', cards: 2, width: 'w-[21rem] sm:w-[23rem]' },
11
+ { id: 'blocked', cards: 1, width: 'w-[20rem] sm:w-[22rem]' },
12
+ ] as const;
13
+
14
+ function KanbanCardSkeleton({ compact = false }: { compact?: boolean }) {
15
+ return (
16
+ <div className="rounded-lg border bg-card/80 p-3 shadow-xs">
17
+ <div className="flex items-start justify-between gap-3">
18
+ <div className="min-w-0 flex-1 space-y-2">
19
+ <Skeleton className="h-4 w-4/5" />
20
+ {!compact && <Skeleton className="h-4 w-3/5" />}
21
+ </div>
22
+ <Skeleton className="h-4 w-4 rounded" />
23
+ </div>
24
+ <div className="mt-3 flex items-center gap-2">
25
+ <Skeleton className="h-4 w-20 rounded-full" />
26
+ <Skeleton className="h-4 w-14 rounded-full" />
27
+ {!compact && <Skeleton className="h-4 w-16 rounded-full" />}
28
+ </div>
29
+ <div className="mt-3 flex items-center gap-2">
30
+ <Skeleton className="h-3 w-24" />
31
+ <Skeleton className="h-3 w-16" />
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ function KanbanColumnSkeleton({
38
+ cards,
39
+ className,
40
+ }: {
41
+ cards: number;
42
+ className?: string;
43
+ }) {
44
+ return (
45
+ <div
46
+ className={cn(
47
+ 'flex h-full shrink-0 flex-col overflow-hidden rounded-xl border bg-muted/20',
48
+ className
49
+ )}
50
+ >
51
+ <div className="flex h-14 shrink-0 items-center justify-between border-b px-3">
52
+ <div className="flex min-w-0 items-center gap-2">
53
+ <Skeleton className="h-4 w-4 rounded" />
54
+ <Skeleton className="h-4 w-28" />
55
+ <Skeleton className="h-5 w-7 rounded-full" />
56
+ </div>
57
+ <Skeleton className="h-6 w-6 rounded-md" />
58
+ </div>
59
+ <div className="min-h-0 flex-1 space-y-3 overflow-hidden p-3">
60
+ {Array.from({ length: cards }).map((_, index) => (
61
+ <KanbanCardSkeleton
62
+ compact={index === cards - 1 && cards > 1}
63
+ key={`${cards}-${index}`}
64
+ />
65
+ ))}
66
+ <div className="mt-auto pt-3">
67
+ <Skeleton className="h-9 w-full rounded-lg border border-dashed bg-transparent" />
68
+ </div>
69
+ </div>
70
+ </div>
71
+ );
72
+ }
4
73
 
5
74
  export function KanbanSkeleton() {
6
75
  return (
7
- <div className="flex h-full flex-col">
8
- {/* Loading skeleton for search bar */}
9
- <Card className="mb-4 border-dynamic-blue/20 bg-dynamic-blue/5">
10
- <CardContent className="p-4">
11
- <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
12
- <div className="relative max-w-md flex-1">
13
- <div className="h-9 w-full animate-pulse rounded-md bg-dynamic-blue/10"></div>
14
- </div>
15
- <div className="flex items-center gap-2">
16
- <div className="h-8 w-16 animate-pulse rounded-md bg-dynamic-blue/10"></div>
17
- <div className="h-8 w-20 animate-pulse rounded-md bg-dynamic-blue/10"></div>
76
+ <div
77
+ aria-hidden="true"
78
+ className="h-full overflow-hidden bg-transparent"
79
+ data-testid="kanban-skeleton"
80
+ >
81
+ <div className="flex h-full gap-2 overflow-hidden p-2 sm:gap-3">
82
+ <div className="hidden shrink-0 gap-2 sm:flex">
83
+ {RAILS.map((rail) => (
84
+ <div
85
+ className="flex h-full w-10 flex-col items-center rounded-xl border border-dashed bg-muted/10 p-2"
86
+ key={rail}
87
+ >
88
+ <Skeleton className="h-4 w-4 rounded" />
89
+ <Skeleton className="mt-5 h-5 w-5 rounded-full" />
90
+ <Skeleton className="mt-6 h-28 w-3 rounded-full" />
18
91
  </div>
19
- </div>
20
- </CardContent>
21
- </Card>
92
+ ))}
93
+ </div>
22
94
 
23
- {/* Loading skeleton for kanban columns */}
24
- <div className="flex-1 overflow-x-auto overflow-y-hidden">
25
- <div className="flex h-full gap-4 p-4">
26
- {[1, 2, 3, 4].map((i) => (
27
- <Card key={i} className="h-full w-87.5 animate-pulse">
28
- <div className="p-4">
29
- <div className="mb-4 h-6 w-32 rounded bg-muted"></div>
30
- <div className="space-y-3">
31
- {[1, 2, 3].map((j) => (
32
- <div
33
- key={j}
34
- className="h-24 w-full rounded bg-muted/50"
35
- ></div>
36
- ))}
37
- </div>
38
- </div>
39
- </Card>
95
+ <div className="flex min-w-0 flex-1 gap-3 overflow-hidden">
96
+ {COLUMNS.map((column) => (
97
+ <KanbanColumnSkeleton
98
+ cards={column.cards}
99
+ className={column.width}
100
+ key={column.id}
101
+ />
40
102
  ))}
41
103
  </div>
42
104
  </div>
@@ -15,6 +15,7 @@ import {
15
15
  } from '@dnd-kit/core';
16
16
  import { useQuery, useQueryClient } from '@tanstack/react-query';
17
17
  import { updateWorkspaceTaskList } from '@tuturuuu/internal-api';
18
+ import type { ListWorkspaceTasksOptions } from '@tuturuuu/internal-api/tasks';
18
19
  import type { Workspace, WorkspaceProductTier } from '@tuturuuu/types';
19
20
  import type { Task } from '@tuturuuu/types/primitives/Task';
20
21
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
@@ -28,12 +29,20 @@ import { useOptionalWorkspacePresenceContext } from '../../providers/workspace-p
28
29
  import { useBoardBroadcast } from '../../shared/board-broadcast-context';
29
30
  import type { ListStatusFilter } from '../../shared/board-header';
30
31
  import { buildEstimationIndices } from '../../shared/estimation-mapping';
32
+ import type {
33
+ SpecialTaskListPin,
34
+ SpecialTaskListPinState,
35
+ } from '../../shared/special-task-list-pins';
36
+ import { TaskBoardLoadingState } from '../../shared/task-board-loading-state';
31
37
  import { BoardSelector } from '../board-selector';
32
38
  import { BulkActionsIsland } from './kanban/bulk/bulk-actions-island';
33
39
  import { BulkCustomDateDialog } from './kanban/bulk/bulk-custom-date-dialog';
34
40
  import { BulkDeleteDialog } from './kanban/bulk/bulk-delete-dialog';
35
41
  import { useBulkOperations } from './kanban/bulk/bulk-operations';
36
- import { listKanbanDeadlineTasks } from './kanban/data/kanban-deadline-query';
42
+ import {
43
+ getKanbanDeadlineTasksQueryKey,
44
+ listKanbanDeadlineTasks,
45
+ } from './kanban/data/kanban-deadline-query';
37
46
  import { useAppliedSets } from './kanban/data/use-applied-sets';
38
47
  import { useBulkResources } from './kanban/data/use-bulk-resources';
39
48
  import { useFilteredResources } from './kanban/data/use-filtered-resources';
@@ -42,8 +51,11 @@ import { DragPreview } from './kanban/dnd/drag-preview';
42
51
  import { useKanbanDnd } from './kanban/dnd/use-kanban-dnd';
43
52
  import { DRAG_ACTIVATION_DISTANCE } from './kanban/kanban-constants';
44
53
  import { KanbanColumns } from './kanban/rendering/kanban-columns';
54
+ import type {
55
+ KanbanDeadlineCollapsedState,
56
+ KanbanDeadlineSection,
57
+ } from './kanban/rendering/kanban-deadline-panels';
45
58
  import { buildKanbanDeadlineSections } from './kanban/rendering/kanban-deadline-tasks';
46
- import { KanbanSkeleton } from './kanban/rendering/kanban-skeleton';
47
59
  import { useKeyboardShortcuts } from './kanban/selection/use-keyboard-shortcuts';
48
60
  import { useMultiSelect } from './kanban/selection/use-multi-select';
49
61
  import type { TaskFilters } from './task-filter';
@@ -61,6 +73,8 @@ const kanbanCollisionDetection: CollisionDetection = (args) => {
61
73
  return closestCenter(args);
62
74
  };
63
75
 
76
+ const DEADLINE_REFRESH_INTERVAL_MS = 60_000;
77
+
64
78
  interface Props {
65
79
  workspace: Workspace;
66
80
  workspaceTier?: WorkspaceProductTier | null;
@@ -72,11 +86,23 @@ interface Props {
72
86
  disableSort?: boolean;
73
87
  listStatusFilter?: ListStatusFilter;
74
88
  filters?: TaskFilters;
89
+ deadlineTaskQueryOptions?: ListWorkspaceTasksOptions;
75
90
  isMultiSelectMode: boolean;
76
91
  setIsMultiSelectMode: (enabled: boolean) => void;
77
92
  onExternalTasksCollapsedChange?: (collapsed: boolean) => void;
78
93
  onTaskListCollapsedChange?: (listId: string, collapsed: boolean) => void;
94
+ deadlineSectionsCollapsed?: KanbanDeadlineCollapsedState;
95
+ onDeadlineSectionCollapsedChange?: (
96
+ section: KanbanDeadlineSection,
97
+ collapsed: boolean
98
+ ) => void;
99
+ specialTaskListPins?: SpecialTaskListPinState;
100
+ onSpecialTaskListPinnedChange?: (
101
+ pin: SpecialTaskListPin,
102
+ pinned: boolean
103
+ ) => void;
79
104
  onBulkSelectionActiveChange?: (active: boolean) => void;
105
+ readOnly?: boolean;
80
106
  }
81
107
 
82
108
  export function KanbanBoard({
@@ -89,12 +115,20 @@ export function KanbanBoard({
89
115
  disableSort = false,
90
116
  listStatusFilter = 'all',
91
117
  filters,
118
+ deadlineTaskQueryOptions,
92
119
  isMultiSelectMode,
93
120
  setIsMultiSelectMode,
94
121
  onExternalTasksCollapsedChange,
95
122
  onTaskListCollapsedChange,
123
+ deadlineSectionsCollapsed,
124
+ onDeadlineSectionCollapsedChange,
125
+ specialTaskListPins,
126
+ onSpecialTaskListPinnedChange,
96
127
  onBulkSelectionActiveChange,
128
+ readOnly = false,
97
129
  }: Props) {
130
+ const tCommon = useTranslations('common');
131
+ const tBoards = useTranslations('ws-task-boards');
98
132
  const tLayout = useTranslations('ws-task-boards.layout_settings');
99
133
  const tTasks = useTranslations('ws-tasks');
100
134
  const invalidColumnMoveMessage = tLayout.has('cannot_reorder_across_statuses')
@@ -104,6 +138,7 @@ export function KanbanBoard({
104
138
  const [bulkWorking, setBulkWorking] = useState(false);
105
139
  const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false);
106
140
  const [bulkCustomDateOpen, setBulkCustomDateOpen] = useState(false);
141
+ const [deadlineNow, setDeadlineNow] = useState(() => Date.now());
107
142
 
108
143
  // Search state
109
144
  const [labelSearchQuery, setLabelSearchQuery] = useState('');
@@ -118,6 +153,7 @@ export function KanbanBoard({
118
153
  const queryClient = useQueryClient();
119
154
  const wsPresence = useOptionalWorkspacePresenceContext();
120
155
  const cursorsEnabled =
156
+ !readOnly &&
121
157
  !workspace.personal &&
122
158
  !!boardId &&
123
159
  !!wsPresence?.cursorsEnabled &&
@@ -127,20 +163,29 @@ export function KanbanBoard({
127
163
  const { createTask } = useTaskDialog();
128
164
  const { weekStartsOn } = useCalendarPreferences();
129
165
 
130
- const { data: boardConfig } = useBoardConfig(boardId, workspaceId);
166
+ const { data: boardConfig } = useBoardConfig(
167
+ readOnly ? null : boardId,
168
+ readOnly ? null : workspaceId
169
+ );
131
170
  const { data: deadlineTasks = [] } = useQuery({
132
- enabled: Boolean(boardId),
171
+ enabled: Boolean(boardId) && !readOnly,
133
172
  queryFn: () =>
134
173
  listKanbanDeadlineTasks({
135
174
  boardId: boardId ?? '',
175
+ taskQueryOptions: deadlineTaskQueryOptions,
136
176
  workspaceId,
137
177
  }),
138
- queryKey: ['kanban-deadline-tasks', workspaceId, boardId],
178
+ queryKey: getKanbanDeadlineTasksQueryKey(
179
+ workspaceId,
180
+ boardId,
181
+ deadlineTaskQueryOptions
182
+ ),
139
183
  staleTime: 30_000,
140
184
  });
141
185
  const persistListPositions = useCallback(
142
186
  async (updates: Array<{ listId: string; newPosition: number }>) => {
143
187
  if (!boardId || updates.length === 0) return;
188
+ if (readOnly) return;
144
189
 
145
190
  await Promise.all(
146
191
  updates.map(({ listId, newPosition }) =>
@@ -150,7 +195,7 @@ export function KanbanBoard({
150
195
  )
151
196
  );
152
197
  },
153
- [boardId, workspaceId]
198
+ [boardId, readOnly, workspaceId]
154
199
  );
155
200
 
156
201
  const columns: TaskList[] = lists.map((list) => ({
@@ -158,7 +203,28 @@ export function KanbanBoard({
158
203
  title: list.name,
159
204
  }));
160
205
 
161
- const orderedColumns = useMemo(() => sortKanbanColumns(columns), [columns]);
206
+ const orderedColumns = useMemo(() => {
207
+ const sortedColumns = sortKanbanColumns(columns);
208
+ const externalColumns = sortedColumns.filter(
209
+ (column) => column.is_external_staging
210
+ );
211
+ const realColumns = sortedColumns.filter(
212
+ (column) => !column.is_external_staging
213
+ );
214
+
215
+ if (!specialTaskListPins?.closed_tasks) {
216
+ return [...externalColumns, ...realColumns];
217
+ }
218
+
219
+ const closedColumns = realColumns.filter(
220
+ (column) => column.status === 'closed'
221
+ );
222
+ const otherColumns = realColumns.filter(
223
+ (column) => column.status !== 'closed'
224
+ );
225
+
226
+ return [...externalColumns, ...closedColumns, ...otherColumns];
227
+ }, [columns, specialTaskListPins?.closed_tasks]);
162
228
  const orderedRealColumns = useMemo(
163
229
  () => orderedColumns.filter((column) => !column.is_external_staging),
164
230
  [orderedColumns]
@@ -172,22 +238,38 @@ export function KanbanBoard({
172
238
  buildKanbanDeadlineSections({
173
239
  deadlineTasks,
174
240
  lists: orderedColumns,
241
+ now: new Date(deadlineNow),
175
242
  visibleTasks: tasks,
176
243
  }),
177
- [deadlineTasks, orderedColumns, tasks]
244
+ [deadlineNow, deadlineTasks, orderedColumns, tasks]
178
245
  );
179
246
  const deadlineLabels = useMemo(
180
247
  () => ({
248
+ collapseSection: (name: string) => tTasks('collapse_task_list', { name }),
249
+ expandSection: (name: string) => tTasks('expand_task_list', { name }),
250
+ filter: tCommon('filters'),
181
251
  overdue: tTasks('overdue'),
252
+ pinSection: (name: string) => tTasks('pin_task_list', { name }),
253
+ reset: tCommon('reset'),
254
+ showDocuments: tTasks('external_tasks_show_documents'),
255
+ showExternalTasks: tTasks('external_tasks'),
256
+ sort: tCommon('sort'),
257
+ sortCreatedAsc: tBoards('filters.sort_options.oldest_first'),
258
+ sortCreatedDesc: tBoards('filters.sort_options.newest_first'),
259
+ sortDueAsc: tBoards('filters.sort_options.soonest_first'),
260
+ sortDueDesc: tBoards('filters.sort_options.latest_first'),
261
+ sortNameAsc: tTasks('external_tasks_sort_name_asc'),
262
+ sortSourceAsc: tTasks('external_tasks_sort_source_asc'),
263
+ unpinSection: (name: string) => tTasks('unpin_task_list', { name }),
182
264
  upcoming: tTasks('upcoming'),
183
265
  }),
184
- [tTasks]
266
+ [tBoards, tCommon, tTasks]
185
267
  );
186
268
 
187
269
  // Selection Hook
188
270
  const { selectedTasks, handleTaskSelect, clearSelection } = useMultiSelect(
189
271
  tasks,
190
- isMultiSelectMode,
272
+ readOnly ? false : isMultiSelectMode,
191
273
  setIsMultiSelectMode
192
274
  );
193
275
 
@@ -195,6 +277,16 @@ export function KanbanBoard({
195
277
  onBulkSelectionActiveChange?.(selectedTasks.size > 0);
196
278
  }, [onBulkSelectionActiveChange, selectedTasks.size]);
197
279
 
280
+ useEffect(() => {
281
+ if (readOnly) return;
282
+
283
+ const interval = window.setInterval(() => {
284
+ setDeadlineNow(Date.now());
285
+ }, DEADLINE_REFRESH_INTERVAL_MS);
286
+
287
+ return () => window.clearInterval(interval);
288
+ }, [readOnly]);
289
+
198
290
  useEffect(
199
291
  () => () => {
200
292
  onBulkSelectionActiveChange?.(false);
@@ -270,12 +362,13 @@ export function KanbanBoard({
270
362
  columns: orderedRealColumns,
271
363
  boardId,
272
364
  filters,
273
- selectedTasks,
274
- isMultiSelectMode,
275
- setIsMultiSelectMode,
276
- createTask,
365
+ selectedTasks: readOnly ? new Set<string>() : selectedTasks,
366
+ isMultiSelectMode: readOnly ? false : isMultiSelectMode,
367
+ setIsMultiSelectMode: readOnly ? () => {} : setIsMultiSelectMode,
368
+ createTask: readOnly ? () => {} : createTask,
277
369
  clearSelection,
278
370
  handleCrossBoardMove: () => {
371
+ if (readOnly) return;
279
372
  if (selectedTasks.size > 0) {
280
373
  setBoardSelectorOpen(true);
281
374
  }
@@ -360,56 +453,58 @@ export function KanbanBoard({
360
453
  );
361
454
 
362
455
  if (isLoading) {
363
- return <KanbanSkeleton />;
456
+ return <TaskBoardLoadingState />;
364
457
  }
365
458
 
366
459
  return (
367
460
  <div className="flex h-full flex-col">
368
- <BulkActionsIsland
369
- selectedCount={selectedTasks.size}
370
- bulkWorking={bulkWorking}
371
- onClearSelection={clearSelection}
372
- onOpenBoardSelector={() => setBoardSelectorOpen(true)}
373
- menuProps={{
374
- workspace,
375
- boardConfig,
376
- columns: orderedRealColumns,
377
- bulkWorking,
378
- estimationOptions,
379
- appliedSets: appliedSetsMap,
380
- filtered: filteredMap,
381
- search: {
382
- labelQuery: labelSearchQuery,
383
- setLabelQuery: setLabelSearchQuery,
384
- projectQuery: projectSearchQuery,
385
- setProjectQuery: setProjectSearchQuery,
386
- assigneeQuery: assigneeSearchQuery,
387
- setAssigneeQuery: setAssigneeSearchQuery,
388
- },
389
- actions: {
390
- bulkMoveToStatus: (s) => bulkOps.bulkMoveToStatus(s as any),
391
- bulkUpdatePriority: (p) => bulkOps.bulkUpdatePriority(p as any),
392
- bulkUpdateDueDate: (t) => bulkOps.bulkUpdateDueDate(t as any),
393
- bulkUpdateEstimation: bulkOps.bulkUpdateEstimation,
394
- bulkAddLabel: bulkOps.bulkAddLabel,
395
- bulkRemoveLabel: bulkOps.bulkRemoveLabel,
396
- bulkClearLabels: bulkOps.bulkClearLabels,
397
- bulkAddProject: bulkOps.bulkAddProject,
398
- bulkRemoveProject: bulkOps.bulkRemoveProject,
399
- bulkClearProjects: bulkOps.bulkClearProjects,
400
- bulkMoveToList: bulkOps.bulkMoveToList,
401
- bulkAddAssignee: bulkOps.bulkAddAssignee,
402
- bulkRemoveAssignee: bulkOps.bulkRemoveAssignee,
403
- bulkClearAssignees: bulkOps.bulkClearAssignees,
404
- },
405
- onOpenCustomDate: () => setBulkCustomDateOpen(true),
406
- onConfirmDelete: () => setBulkDeleteOpen(true),
407
- }}
408
- />
461
+ {!readOnly && (
462
+ <BulkActionsIsland
463
+ selectedCount={selectedTasks.size}
464
+ bulkWorking={bulkWorking}
465
+ onClearSelection={clearSelection}
466
+ onOpenBoardSelector={() => setBoardSelectorOpen(true)}
467
+ menuProps={{
468
+ workspace,
469
+ boardConfig,
470
+ columns: orderedRealColumns,
471
+ bulkWorking,
472
+ estimationOptions,
473
+ appliedSets: appliedSetsMap,
474
+ filtered: filteredMap,
475
+ search: {
476
+ labelQuery: labelSearchQuery,
477
+ setLabelQuery: setLabelSearchQuery,
478
+ projectQuery: projectSearchQuery,
479
+ setProjectQuery: setProjectSearchQuery,
480
+ assigneeQuery: assigneeSearchQuery,
481
+ setAssigneeQuery: setAssigneeSearchQuery,
482
+ },
483
+ actions: {
484
+ bulkMoveToStatus: (s) => bulkOps.bulkMoveToStatus(s as any),
485
+ bulkUpdatePriority: (p) => bulkOps.bulkUpdatePriority(p as any),
486
+ bulkUpdateDueDate: (t) => bulkOps.bulkUpdateDueDate(t as any),
487
+ bulkUpdateEstimation: bulkOps.bulkUpdateEstimation,
488
+ bulkAddLabel: bulkOps.bulkAddLabel,
489
+ bulkRemoveLabel: bulkOps.bulkRemoveLabel,
490
+ bulkClearLabels: bulkOps.bulkClearLabels,
491
+ bulkAddProject: bulkOps.bulkAddProject,
492
+ bulkRemoveProject: bulkOps.bulkRemoveProject,
493
+ bulkClearProjects: bulkOps.bulkClearProjects,
494
+ bulkMoveToList: bulkOps.bulkMoveToList,
495
+ bulkAddAssignee: bulkOps.bulkAddAssignee,
496
+ bulkRemoveAssignee: bulkOps.bulkRemoveAssignee,
497
+ bulkClearAssignees: bulkOps.bulkClearAssignees,
498
+ },
499
+ onOpenCustomDate: () => setBulkCustomDateOpen(true),
500
+ onConfirmDelete: () => setBulkDeleteOpen(true),
501
+ }}
502
+ />
503
+ )}
409
504
 
410
505
  <div className="min-h-0 flex-1 overflow-x-auto overflow-y-hidden">
411
506
  <DndContext
412
- sensors={sensors}
507
+ sensors={readOnly ? [] : sensors}
413
508
  collisionDetection={kanbanCollisionDetection}
414
509
  onDragStart={onDragStart}
415
510
  onDragMove={onDragMove}
@@ -430,10 +525,10 @@ export function KanbanBoard({
430
525
  isPersonalWorkspace={workspace.personal}
431
526
  cursorsEnabled={cursorsEnabled}
432
527
  disableSort={disableSort}
433
- selectedTasks={selectedTasks}
434
- isMultiSelectMode={isMultiSelectMode}
435
- setIsMultiSelectMode={setIsMultiSelectMode}
436
- onTaskSelect={handleTaskSelect}
528
+ selectedTasks={readOnly ? new Set<string>() : selectedTasks}
529
+ isMultiSelectMode={readOnly ? false : isMultiSelectMode}
530
+ setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
531
+ onTaskSelect={readOnly ? () => {} : handleTaskSelect}
437
532
  onClearSelection={clearSelection}
438
533
  onUpdate={() => {}} // Optimistic updates handled in DnD
439
534
  dragPreviewPosition={dragPreviewPosition}
@@ -450,58 +545,70 @@ export function KanbanBoard({
450
545
  columnsId={columnsId}
451
546
  deadlineLabels={deadlineLabels}
452
547
  deadlineSections={deadlineSections}
548
+ deadlineSectionsCollapsed={deadlineSectionsCollapsed}
549
+ deadlineNow={deadlineNow}
550
+ onDeadlineSectionCollapsedChange={onDeadlineSectionCollapsedChange}
453
551
  onExternalTasksCollapsedChange={onExternalTasksCollapsedChange}
454
552
  onTaskListCollapsedChange={onTaskListCollapsedChange}
553
+ specialTaskListPins={specialTaskListPins}
554
+ onSpecialTaskListPinnedChange={onSpecialTaskListPinnedChange}
555
+ readOnly={readOnly}
455
556
  />
456
557
 
457
- <DragOverlay dropAnimation={null}>
458
- <DragPreview
459
- activeTask={activeTask}
460
- activeColumn={activeColumn}
461
- tasks={tasks}
462
- columns={orderedColumns}
463
- boardId={boardId ?? ''}
464
- isPersonalWorkspace={workspace.personal}
465
- isMultiSelectMode={isMultiSelectMode}
466
- selectedTasks={selectedTasks}
467
- onUpdate={() => {}}
468
- wsId={workspaceId}
469
- />
470
- </DragOverlay>
558
+ {!readOnly && (
559
+ <DragOverlay dropAnimation={null}>
560
+ <DragPreview
561
+ activeTask={activeTask}
562
+ activeColumn={activeColumn}
563
+ tasks={tasks}
564
+ columns={orderedColumns}
565
+ boardId={boardId ?? ''}
566
+ isPersonalWorkspace={workspace.personal}
567
+ isMultiSelectMode={isMultiSelectMode}
568
+ selectedTasks={selectedTasks}
569
+ onUpdate={() => {}}
570
+ wsId={workspaceId}
571
+ />
572
+ </DragOverlay>
573
+ )}
471
574
  </DndContext>
472
575
  </div>
473
576
 
474
- <BoardSelector
475
- open={boardSelectorOpen}
476
- onOpenChange={setBoardSelectorOpen}
477
- wsId={workspaceId}
478
- currentBoardId={boardId ?? ''}
479
- taskCount={selectedTasks.size}
480
- onMove={handleBoardMove}
481
- isMoving={bulkWorking}
482
- />
483
-
484
- <BulkDeleteDialog
485
- open={bulkDeleteOpen}
486
- onOpenChange={setBulkDeleteOpen}
487
- selectedCount={selectedTasks.size}
488
- onConfirm={bulkOps.bulkDeleteTasks}
489
- isLoading={bulkWorking}
490
- />
491
-
492
- <BulkCustomDateDialog
493
- open={bulkCustomDateOpen}
494
- onOpenChange={setBulkCustomDateOpen}
495
- onDateChange={(date) => {
496
- bulkOps.bulkUpdateCustomDueDate(date ?? null);
497
- setBulkCustomDateOpen(false);
498
- }}
499
- onClear={() => {
500
- bulkOps.bulkUpdateDueDate('clear');
501
- setBulkCustomDateOpen(false);
502
- }}
503
- isLoading={bulkWorking}
504
- />
577
+ {!readOnly && (
578
+ <>
579
+ <BoardSelector
580
+ open={boardSelectorOpen}
581
+ onOpenChange={setBoardSelectorOpen}
582
+ wsId={workspaceId}
583
+ currentBoardId={boardId ?? ''}
584
+ taskCount={selectedTasks.size}
585
+ onMove={handleBoardMove}
586
+ isMoving={bulkWorking}
587
+ />
588
+
589
+ <BulkDeleteDialog
590
+ open={bulkDeleteOpen}
591
+ onOpenChange={setBulkDeleteOpen}
592
+ selectedCount={selectedTasks.size}
593
+ onConfirm={bulkOps.bulkDeleteTasks}
594
+ isLoading={bulkWorking}
595
+ />
596
+
597
+ <BulkCustomDateDialog
598
+ open={bulkCustomDateOpen}
599
+ onOpenChange={setBulkCustomDateOpen}
600
+ onDateChange={(date) => {
601
+ bulkOps.bulkUpdateCustomDueDate(date ?? null);
602
+ setBulkCustomDateOpen(false);
603
+ }}
604
+ onClear={() => {
605
+ bulkOps.bulkUpdateDueDate('clear');
606
+ setBulkCustomDateOpen(false);
607
+ }}
608
+ isLoading={bulkWorking}
609
+ />
610
+ </>
611
+ )}
505
612
  </div>
506
613
  );
507
614
  }