@tuturuuu/ui 0.8.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 (245) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/biome.json +1 -1
  3. package/package.json +74 -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-search.test.ts +78 -0
  29. package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
  30. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  31. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +46 -1
  32. package/src/components/ui/custom/combobox.test.tsx +195 -0
  33. package/src/components/ui/custom/combobox.tsx +273 -156
  34. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  35. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  36. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  37. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  38. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  39. package/src/components/ui/custom/nav-link.test.tsx +165 -0
  40. package/src/components/ui/custom/nav-link.tsx +69 -11
  41. package/src/components/ui/custom/navigation.tsx +1 -0
  42. package/src/components/ui/custom/settings/task-settings.tsx +104 -0
  43. package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
  44. package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
  45. package/src/components/ui/custom/settings-dialog-search.ts +75 -0
  46. package/src/components/ui/custom/settings-dialog-shell.tsx +65 -28
  47. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  48. package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
  49. package/src/components/ui/custom/workspace-select.tsx +25 -19
  50. package/src/components/ui/dialog.test.tsx +52 -0
  51. package/src/components/ui/dialog.tsx +6 -2
  52. package/src/components/ui/dropdown-menu.tsx +5 -1
  53. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  54. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  55. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  56. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  57. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  58. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  59. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  60. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  61. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  62. package/src/components/ui/finance/invoices/utils.ts +3 -1
  63. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  64. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  65. package/src/components/ui/finance/transactions/form.tsx +2 -0
  66. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  67. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  68. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  69. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  70. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  71. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  72. package/src/components/ui/finance/wallets/form.tsx +15 -4
  73. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  75. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  76. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  77. package/src/components/ui/input-otp.tsx +1 -1
  78. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  79. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  80. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  81. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  82. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  83. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  84. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  85. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  86. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  87. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  88. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  89. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  90. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  91. package/src/components/ui/navigation-menu.tsx +1 -1
  92. package/src/components/ui/pagination.tsx +1 -1
  93. package/src/components/ui/radio-group.tsx +1 -1
  94. package/src/components/ui/select.tsx +5 -1
  95. package/src/components/ui/sheet.tsx +1 -1
  96. package/src/components/ui/sidebar.tsx +1 -1
  97. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  98. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  99. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  100. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  101. package/src/components/ui/storefront/listing-card.tsx +1 -1
  102. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  103. package/src/components/ui/storefront/product-detail.tsx +1 -1
  104. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  105. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  106. package/src/components/ui/storefront/types.ts +4 -0
  107. package/src/components/ui/storefront/utils.ts +6 -0
  108. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  109. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  110. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  111. package/src/components/ui/text-editor/editor.tsx +69 -14
  112. package/src/components/ui/text-editor/extensions.ts +8 -2
  113. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  114. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  115. package/src/components/ui/toast.tsx +1 -1
  116. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +286 -0
  117. package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
  118. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  119. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +15 -226
  120. package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
  121. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +121 -39
  122. package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
  127. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  128. package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
  129. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  131. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
  132. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  133. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  134. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  135. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  136. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  139. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  141. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  142. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  143. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +642 -5
  148. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +224 -15
  149. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +535 -53
  150. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +101 -33
  151. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +235 -113
  152. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +50 -5
  153. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +12 -2
  154. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +10 -1
  155. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  156. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
  157. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
  158. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +271 -36
  159. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  160. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  161. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +22 -0
  162. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
  163. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
  164. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
  165. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
  166. package/src/components/ui/tu-do/boards/form.tsx +1 -1
  167. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  168. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  169. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  170. package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
  171. package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
  172. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
  173. package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
  174. package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
  175. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  176. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  177. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  178. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  179. package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
  180. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
  181. package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
  182. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
  183. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
  184. package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
  185. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +141 -1
  186. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +377 -36
  187. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +374 -0
  188. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +419 -5
  189. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +38 -0
  190. package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
  191. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  192. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
  193. package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
  194. package/src/components/ui/tu-do/shared/board-client.tsx +15 -10
  195. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  196. package/src/components/ui/tu-do/shared/board-header.tsx +471 -975
  197. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  198. package/src/components/ui/tu-do/shared/board-switcher.tsx +244 -220
  199. package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
  200. package/src/components/ui/tu-do/shared/board-views.tsx +577 -85
  201. package/src/components/ui/tu-do/shared/list-view.tsx +246 -2
  202. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  203. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  204. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  205. package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
  206. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
  210. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  211. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
  212. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
  213. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
  214. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
  215. package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
  216. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
  217. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
  218. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
  219. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
  220. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
  221. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  222. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  223. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  224. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
  225. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
  226. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
  227. package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
  228. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  229. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +11 -1
  230. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  231. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
  232. package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
  233. package/src/declarations.d.ts +1 -0
  234. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  235. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  236. package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
  237. package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
  238. package/src/hooks/use-calendar-sync.tsx +247 -243
  239. package/src/hooks/use-calendar.tsx +323 -138
  240. package/src/hooks/use-task-actions.ts +24 -0
  241. package/src/hooks/use-user-workspace-config.ts +75 -0
  242. package/src/hooks/use-workspace-currency.ts +8 -3
  243. package/src/hooks/useBoardPresence.ts +364 -0
  244. package/src/hooks/useBoardRealtimeEventHandler.ts +45 -90
  245. package/src/lib/workspace-actions.ts +2 -6
@@ -10,6 +10,10 @@ import {
10
10
  type ListWorkspaceTasksOptions,
11
11
  listWorkspaceTasks,
12
12
  } from '@tuturuuu/internal-api/tasks';
13
+ import {
14
+ TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
15
+ TASK_LAST_BOARD_VIEW_CONFIG_ID,
16
+ } from '@tuturuuu/internal-api/users';
13
17
  import type {
14
18
  Workspace,
15
19
  WorkspaceProductTier,
@@ -19,6 +23,7 @@ import type { Task } from '@tuturuuu/types/primitives/Task';
19
23
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
20
24
  import {
21
25
  getPersonalExternalStagingListId,
26
+ priorityCompare,
22
27
  type WorkspaceLabel,
23
28
  } from '@tuturuuu/utils/task-helper';
24
29
  import { useTranslations } from 'next-intl';
@@ -30,25 +35,51 @@ import {
30
35
  useMemo,
31
36
  useState,
32
37
  } from 'react';
38
+ import {
39
+ useUpdateUserWorkspaceConfig,
40
+ useUserWorkspaceConfig,
41
+ } from '../../../../hooks/use-user-workspace-config';
33
42
  import { KanbanBoard } from '../boards/boardId/kanban';
43
+ import type {
44
+ KanbanDeadlineCollapsedState,
45
+ KanbanDeadlineSection,
46
+ } from '../boards/boardId/kanban/rendering/kanban-deadline-panels';
34
47
  import type { TaskFilters } from '../boards/boardId/task-filter';
35
48
  import { TimelineBoard } from '../boards/boardId/timeline-board';
49
+ import { DraftsPage } from '../drafts/drafts-page';
36
50
  import { useTaskDialog } from '../hooks/useTaskDialog';
51
+ import MyTasksContent from '../my-tasks/my-tasks-content';
37
52
  import { BoardHeader, type ListStatusFilter } from '../shared/board-header';
38
53
  import { ListView } from '../shared/list-view';
39
- import { RecycleBinPanel } from '../shared/recycle-bin-panel';
54
+ import { RecycleBinContent } from '../shared/recycle-bin-panel';
40
55
  import { loadBoardConfig } from './board-config-storage';
56
+ import {
57
+ parseSpecialTaskListPins,
58
+ type SpecialTaskListPin,
59
+ serializeSpecialTaskListPins,
60
+ } from './special-task-list-pins';
41
61
 
42
- export type ViewType = 'kanban' | 'list' | 'timeline';
62
+ export type ViewType =
63
+ | 'kanban'
64
+ | 'list'
65
+ | 'my_tasks'
66
+ | 'timeline'
67
+ | 'drafts'
68
+ | 'recycle_bin';
43
69
 
44
70
  const HOTKEY_CREATE_TASK = 'C';
45
71
  const HOTKEY_GO_TO_KANBAN: ['G', 'K'] = ['G', 'K'];
46
72
  const HOTKEY_GO_TO_LIST: ['G', 'L'] = ['G', 'L'];
73
+ const HOTKEY_GO_TO_MY_TASKS: ['G', 'M'] = ['G', 'M'];
47
74
  const HOTKEY_GO_TO_TIMELINE: ['G', 'T'] = ['G', 'T'];
75
+ const HOTKEY_GO_TO_DRAFTS: ['G', 'D'] = ['G', 'D'];
76
+ const HOTKEY_GO_TO_RECYCLE_BIN: ['G', 'R'] = ['G', 'R'];
48
77
  const EXTERNAL_TASKS_COLLAPSED_STORAGE_PREFIX =
49
78
  'personal-board-external-tasks-collapsed';
50
79
  const CLOSED_TASK_LIST_COLLAPSED_STORAGE_PREFIX =
51
80
  'task-board-closed-list-collapsed';
81
+ const DEADLINE_SECTION_COLLAPSED_STORAGE_PREFIX =
82
+ 'task-board-deadline-section-collapsed';
52
83
  const DEFAULT_TASK_FILTERS: TaskFilters = {
53
84
  labels: [],
54
85
  assignees: [],
@@ -63,31 +94,163 @@ const DEFAULT_TASK_FILTERS: TaskFilters = {
63
94
  sourceBoardIds: [],
64
95
  };
65
96
 
66
- function hasAssignedExternalTasks(tasks: Task[], boardId: string) {
67
- const externalStagingListId = getPersonalExternalStagingListId(boardId);
97
+ function getClosedTaskListCollapsedStorageKey(boardId: string, listId: string) {
98
+ return `${CLOSED_TASK_LIST_COLLAPSED_STORAGE_PREFIX}:${boardId}:${listId}`;
99
+ }
68
100
 
69
- return tasks.some(
70
- (task) =>
71
- task.is_personal_external === true ||
72
- task.list_id === externalStagingListId ||
73
- Boolean(task.source_workspace_id)
74
- );
101
+ function getDeadlineSectionCollapsedStorageKey(
102
+ boardId: string,
103
+ section: KanbanDeadlineSection
104
+ ) {
105
+ return `${DEADLINE_SECTION_COLLAPSED_STORAGE_PREFIX}:${boardId}:${section}`;
75
106
  }
76
107
 
77
- function getClosedTaskListCollapsedStorageKey(boardId: string, listId: string) {
78
- return `${CLOSED_TASK_LIST_COLLAPSED_STORAGE_PREFIX}:${boardId}:${listId}`;
108
+ function taskMatchesLocalFilters(
109
+ task: Task,
110
+ filters: TaskFilters,
111
+ currentUserId?: string
112
+ ) {
113
+ const query = filters.searchQuery?.trim().toLowerCase();
114
+ if (query) {
115
+ const searchableText = [
116
+ task.name,
117
+ task.display_number ? String(task.display_number) : null,
118
+ ...(task.labels ?? []).map((label) => label.name),
119
+ ...(task.projects ?? []).map((project) => project.name),
120
+ ...(task.assignees ?? []).map(
121
+ (assignee) => assignee.display_name ?? assignee.email ?? assignee.handle
122
+ ),
123
+ ]
124
+ .filter(Boolean)
125
+ .join(' ')
126
+ .toLowerCase();
127
+
128
+ if (!searchableText.includes(query)) return false;
129
+ }
130
+
131
+ if (
132
+ filters.labels.length > 0 &&
133
+ !filters.labels.every((label) =>
134
+ task.labels?.some((taskLabel) => taskLabel.id === label.id)
135
+ )
136
+ ) {
137
+ return false;
138
+ }
139
+
140
+ if (
141
+ filters.projects.length > 0 &&
142
+ !filters.projects.every((project) =>
143
+ task.projects?.some((taskProject) => taskProject.id === project.id)
144
+ )
145
+ ) {
146
+ return false;
147
+ }
148
+
149
+ if (
150
+ filters.priorities.length > 0 &&
151
+ (!task.priority || !filters.priorities.includes(task.priority))
152
+ ) {
153
+ return false;
154
+ }
155
+
156
+ if (filters.assignees.length > 0) {
157
+ const assigneeIds = new Set(
158
+ filters.assignees.map((assignee) => assignee.id)
159
+ );
160
+ if (!task.assignees?.some((assignee) => assigneeIds.has(assignee.id))) {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ if (
166
+ filters.includeMyTasks &&
167
+ currentUserId &&
168
+ !task.assignees?.some((assignee) => assignee.id === currentUserId)
169
+ ) {
170
+ return false;
171
+ }
172
+
173
+ if (filters.includeUnassigned && (task.assignees?.length ?? 0) > 0) {
174
+ return false;
175
+ }
176
+
177
+ if (filters.dueDateRange?.from || filters.dueDateRange?.to) {
178
+ if (!task.end_date) return false;
179
+ const dueTime = new Date(task.end_date).getTime();
180
+ const fromTime = filters.dueDateRange.from?.getTime() ?? -Infinity;
181
+ const toTime = filters.dueDateRange.to?.getTime() ?? Infinity;
182
+ if (dueTime < fromTime || dueTime > toTime) return false;
183
+ }
184
+
185
+ if (
186
+ typeof filters.estimationRange?.min === 'number' ||
187
+ typeof filters.estimationRange?.max === 'number'
188
+ ) {
189
+ const estimate = task.estimation_points ?? 0;
190
+ const min = filters.estimationRange.min ?? -Infinity;
191
+ const max = filters.estimationRange.max ?? Infinity;
192
+ if (estimate < min || estimate > max) return false;
193
+ }
194
+
195
+ return true;
196
+ }
197
+
198
+ function getTaskTimestamp(value: string | null | undefined) {
199
+ if (!value) return Number.MAX_SAFE_INTEGER;
200
+ const time = new Date(value).getTime();
201
+ return Number.isFinite(time) ? time : Number.MAX_SAFE_INTEGER;
202
+ }
203
+
204
+ function sortLocalTasks(tasks: Task[], sortBy: TaskFilters['sortBy']) {
205
+ if (!sortBy) return tasks;
206
+
207
+ return [...tasks].sort((a, b) => {
208
+ switch (sortBy) {
209
+ case 'name-asc':
210
+ return a.name.localeCompare(b.name);
211
+ case 'name-desc':
212
+ return b.name.localeCompare(a.name);
213
+ case 'priority-high':
214
+ return priorityCompare(a.priority ?? null, b.priority ?? null);
215
+ case 'priority-low':
216
+ return priorityCompare(b.priority ?? null, a.priority ?? null);
217
+ case 'due-date-asc':
218
+ return getTaskTimestamp(a.end_date) - getTaskTimestamp(b.end_date);
219
+ case 'due-date-desc':
220
+ return getTaskTimestamp(b.end_date) - getTaskTimestamp(a.end_date);
221
+ case 'created-date-asc':
222
+ return getTaskTimestamp(a.created_at) - getTaskTimestamp(b.created_at);
223
+ case 'created-date-desc':
224
+ return getTaskTimestamp(b.created_at) - getTaskTimestamp(a.created_at);
225
+ case 'estimation-high':
226
+ return (b.estimation_points ?? 0) - (a.estimation_points ?? 0);
227
+ case 'estimation-low':
228
+ return (a.estimation_points ?? 0) - (b.estimation_points ?? 0);
229
+ default:
230
+ return 0;
231
+ }
232
+ });
79
233
  }
80
234
 
81
235
  interface Props {
82
236
  workspace: Workspace;
83
237
  workspaceTier?: WorkspaceProductTier | null;
84
- board: WorkspaceTaskBoard;
238
+ board: WorkspaceTaskBoard & {
239
+ access_type?: 'member' | 'guest';
240
+ guest_permission?: 'view' | 'edit' | null;
241
+ has_guest_access?: boolean;
242
+ };
85
243
  tasks: Task[];
86
244
  lists: TaskList[];
87
245
  workspaceLabels: WorkspaceLabel[];
246
+ availableViews?: ViewType[];
88
247
  canManageBoard?: boolean;
89
248
  currentUserId?: string;
249
+ defaultView?: ViewType;
90
250
  idleBottomIsland?: ReactNode;
251
+ publicHeaderPrefix?: ReactNode;
252
+ publicView?: boolean;
253
+ readOnly?: boolean;
91
254
  }
92
255
 
93
256
  export function BoardViews({
@@ -96,9 +259,14 @@ export function BoardViews({
96
259
  board,
97
260
  tasks,
98
261
  lists,
262
+ availableViews,
99
263
  canManageBoard = true,
100
264
  currentUserId,
265
+ defaultView,
101
266
  idleBottomIsland,
267
+ publicHeaderPrefix,
268
+ publicView = false,
269
+ readOnly = false,
102
270
  }: Props) {
103
271
  const t = useTranslations('common');
104
272
  const tTasks = useTranslations('ws-tasks');
@@ -110,6 +278,8 @@ export function BoardViews({
110
278
  const [closedTaskListsCollapsed, setClosedTaskListsCollapsed] = useState<
111
279
  Record<string, boolean>
112
280
  >({});
281
+ const [deadlineSectionsCollapsed, setDeadlineSectionsCollapsed] =
282
+ useState<KanbanDeadlineCollapsedState>({});
113
283
  const [filters, setFilters] = useState<TaskFilters>(DEFAULT_TASK_FILTERS);
114
284
  const [listStatusFilter, setListStatusFilter] =
115
285
  useState<ListStatusFilter>('all');
@@ -117,11 +287,70 @@ export function BoardViews({
117
287
  const [taskOverrides, setTaskOverrides] = useState<
118
288
  Record<string, Partial<Task>>
119
289
  >({});
120
- const [recycleBinOpen, setRecycleBinOpen] = useState(false);
121
290
  const [isMultiSelectMode, setIsMultiSelectMode] = useState(false);
122
291
  const [kanbanBulkSelectionActive, setKanbanBulkSelectionActive] =
123
292
  useState(false);
124
293
  const { createTask } = useTaskDialog();
294
+ const localTaskState = readOnly || publicView;
295
+ const boardAssigneesEnabled =
296
+ !workspace.personal ||
297
+ board.access_type === 'guest' ||
298
+ board.has_guest_access === true;
299
+ const assigneeMemberSource: 'workspace' | 'board' | 'workspace-and-board' =
300
+ board.access_type === 'guest'
301
+ ? 'board'
302
+ : board.has_guest_access === true
303
+ ? workspace.personal
304
+ ? 'board'
305
+ : 'workspace-and-board'
306
+ : 'workspace';
307
+ const { data: pinnedSpecialListsRaw } = useUserWorkspaceConfig(
308
+ effectiveWorkspaceId,
309
+ TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
310
+ null,
311
+ { enabled: !localTaskState }
312
+ );
313
+ const updateUserWorkspaceConfig = useUpdateUserWorkspaceConfig();
314
+ const specialTaskListPins = useMemo(
315
+ () => parseSpecialTaskListPins(pinnedSpecialListsRaw),
316
+ [pinnedSpecialListsRaw]
317
+ );
318
+ const handleSpecialTaskListPinnedChange = useCallback(
319
+ (pin: SpecialTaskListPin, pinned: boolean) => {
320
+ const nextPins = {
321
+ ...specialTaskListPins,
322
+ [pin]: pinned,
323
+ };
324
+
325
+ if (!pinned) delete nextPins[pin];
326
+
327
+ updateUserWorkspaceConfig.mutate({
328
+ configId: TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
329
+ value: serializeSpecialTaskListPins(nextPins),
330
+ workspaceId: effectiveWorkspaceId,
331
+ });
332
+ },
333
+ [effectiveWorkspaceId, specialTaskListPins, updateUserWorkspaceConfig]
334
+ );
335
+ const enabledViews = useMemo(
336
+ () =>
337
+ availableViews ??
338
+ (publicView || readOnly
339
+ ? (['kanban', 'list', 'timeline'] as ViewType[])
340
+ : ([
341
+ 'kanban',
342
+ 'list',
343
+ 'my_tasks',
344
+ 'timeline',
345
+ 'drafts',
346
+ 'recycle_bin',
347
+ ] as ViewType[])),
348
+ [availableViews, publicView, readOnly]
349
+ );
350
+ const viewIsEnabled = useCallback(
351
+ (view: ViewType) => !enabledViews || enabledViews.includes(view),
352
+ [enabledViews]
353
+ );
125
354
  const sourceScope = filters.sourceScope ?? 'all_visible';
126
355
  const sourceWorkspaceIds = filters.sourceWorkspaceIds ?? [];
127
356
  const sourceBoardIds = filters.sourceBoardIds ?? [];
@@ -206,11 +435,21 @@ export function BoardViews({
206
435
  taskQueryOptions,
207
436
  ]
208
437
  );
438
+ const deadlineTaskQueryOptions = useMemo<ListWorkspaceTasksOptions>(() => {
439
+ const { sortBy: _sortBy, ...filterOptions } = taskQueryOptions;
440
+ return {
441
+ ...filterOptions,
442
+ listStatuses: listStatusesForQuery,
443
+ };
444
+ }, [listStatusesForQuery, taskQueryOptions]);
209
445
  const viewHotkeyLabels = useMemo(
210
446
  () => ({
211
447
  kanban: formatHotkeySequence(HOTKEY_GO_TO_KANBAN),
212
448
  list: formatHotkeySequence(HOTKEY_GO_TO_LIST),
449
+ my_tasks: formatHotkeySequence(HOTKEY_GO_TO_MY_TASKS),
213
450
  timeline: formatHotkeySequence(HOTKEY_GO_TO_TIMELINE),
451
+ drafts: formatHotkeySequence(HOTKEY_GO_TO_DRAFTS),
452
+ recycle_bin: formatHotkeySequence(HOTKEY_GO_TO_RECYCLE_BIN),
214
453
  }),
215
454
  []
216
455
  );
@@ -220,6 +459,7 @@ export function BoardViews({
220
459
  const result = await listWorkspaceTasks(effectiveWorkspaceId, {
221
460
  ...taskQueryOptions,
222
461
  boardId: board.id,
462
+ includeRelationshipSummary: false,
223
463
  listStatuses: listStatusesForQuery,
224
464
  limit: 200,
225
465
  });
@@ -228,6 +468,7 @@ export function BoardViews({
228
468
 
229
469
  const primeFullTaskCache = useCallback(
230
470
  (nextView: ViewType) => {
471
+ if (localTaskState) return;
231
472
  if (nextView !== 'list' && nextView !== 'timeline') return;
232
473
 
233
474
  void queryClient.prefetchQuery({
@@ -236,20 +477,52 @@ export function BoardViews({
236
477
  staleTime: 0,
237
478
  });
238
479
  },
239
- [board.id, fetchBoardTasks, queryClient, taskFilterKey]
480
+ [board.id, fetchBoardTasks, localTaskState, queryClient, taskFilterKey]
240
481
  );
241
482
 
242
483
  const handleViewChange = useCallback(
243
484
  (nextView: ViewType) => {
485
+ if (!viewIsEnabled(nextView)) return;
244
486
  setCurrentView(nextView);
245
487
  primeFullTaskCache(nextView);
488
+ if (!localTaskState) {
489
+ updateUserWorkspaceConfig.mutate({
490
+ configId: TASK_LAST_BOARD_VIEW_CONFIG_ID,
491
+ value: nextView,
492
+ workspaceId: effectiveWorkspaceId,
493
+ });
494
+ }
495
+
496
+ if (typeof window !== 'undefined') {
497
+ const currentUrl = new URL(window.location.href);
498
+ if (!currentUrl.pathname.includes('/tasks/boards/')) return;
499
+
500
+ const params = currentUrl.searchParams;
501
+ if (nextView === 'kanban') {
502
+ params.delete('view');
503
+ } else {
504
+ params.set('view', nextView);
505
+ }
506
+ const nextQuery = params.toString();
507
+ window.history.replaceState(
508
+ window.history.state,
509
+ '',
510
+ `${currentUrl.pathname}${nextQuery ? `?${nextQuery}` : ''}${currentUrl.hash}`
511
+ );
512
+ }
246
513
  },
247
- [primeFullTaskCache]
514
+ [
515
+ effectiveWorkspaceId,
516
+ localTaskState,
517
+ primeFullTaskCache,
518
+ updateUserWorkspaceConfig,
519
+ viewIsEnabled,
520
+ ]
248
521
  );
249
522
 
250
523
  const { data: fullTasks = [], isFetching: isFullTasksFetching } = useQuery({
251
524
  queryKey: ['tasks-full', board.id, taskFilterKey],
252
- enabled: shouldEagerLoadTasks,
525
+ enabled: !localTaskState && shouldEagerLoadTasks,
253
526
  queryFn: fetchBoardTasks,
254
527
  refetchOnMount: 'always',
255
528
  staleTime: 0,
@@ -273,21 +546,40 @@ export function BoardViews({
273
546
 
274
547
  useLayoutEffect(() => {
275
548
  const savedConfig = loadBoardConfig(board.id);
549
+ const requestedView =
550
+ typeof window === 'undefined' ||
551
+ !window.location.pathname.includes('/tasks/boards/')
552
+ ? null
553
+ : (new URLSearchParams(window.location.search).get(
554
+ 'view'
555
+ ) as ViewType | null);
556
+ const fallbackView = enabledViews?.[0] ?? 'kanban';
557
+ const routeDefaultView =
558
+ defaultView && viewIsEnabled(defaultView) ? defaultView : null;
559
+ const effectiveDefaultView = routeDefaultView ?? fallbackView;
560
+ const initialView =
561
+ requestedView && viewIsEnabled(requestedView) ? requestedView : null;
276
562
 
277
563
  if (!savedConfig) {
278
- setCurrentView('kanban');
564
+ setCurrentView(initialView ?? effectiveDefaultView);
279
565
  setFilters(DEFAULT_TASK_FILTERS);
280
566
  setListStatusFilter('all');
281
567
  return;
282
568
  }
283
569
 
284
- setCurrentView(savedConfig.currentView);
570
+ setCurrentView(
571
+ initialView ??
572
+ routeDefaultView ??
573
+ (viewIsEnabled(savedConfig.currentView)
574
+ ? savedConfig.currentView
575
+ : effectiveDefaultView)
576
+ );
285
577
  setFilters({
286
578
  ...DEFAULT_TASK_FILTERS,
287
579
  ...savedConfig.filters,
288
580
  });
289
581
  setListStatusFilter(savedConfig.listStatusFilter);
290
- }, [board.id]);
582
+ }, [board.id, defaultView, enabledViews, viewIsEnabled]);
291
583
 
292
584
  useEffect(() => {
293
585
  if (!workspace.personal || typeof window === 'undefined') {
@@ -301,10 +593,8 @@ export function BoardViews({
301
593
  const storedPreference =
302
594
  storedValue === null ? null : storedValue === 'true';
303
595
 
304
- setExternalTasksCollapsed(
305
- storedPreference ?? !hasAssignedExternalTasks(tasks, board.id)
306
- );
307
- }, [board.id, tasks, workspace.personal]);
596
+ setExternalTasksCollapsed(storedPreference ?? true);
597
+ }, [board.id, workspace.personal]);
308
598
 
309
599
  const handleExternalTasksCollapsedChange = useCallback(
310
600
  (collapsed: boolean) => {
@@ -368,6 +658,45 @@ export function BoardViews({
368
658
  [board.id]
369
659
  );
370
660
 
661
+ useEffect(() => {
662
+ setDeadlineSectionsCollapsed((previous) => {
663
+ const next: KanbanDeadlineCollapsedState = {};
664
+
665
+ for (const section of ['overdue', 'upcoming'] as const) {
666
+ const storedValue =
667
+ typeof window === 'undefined'
668
+ ? null
669
+ : window.localStorage.getItem(
670
+ getDeadlineSectionCollapsedStorageKey(board.id, section)
671
+ );
672
+
673
+ next[section] =
674
+ storedValue === null
675
+ ? (previous[section] ?? true)
676
+ : storedValue === 'true';
677
+ }
678
+
679
+ return next;
680
+ });
681
+ }, [board.id]);
682
+
683
+ const handleDeadlineSectionCollapsedChange = useCallback(
684
+ (section: KanbanDeadlineSection, collapsed: boolean) => {
685
+ setDeadlineSectionsCollapsed((previous) => ({
686
+ ...previous,
687
+ [section]: collapsed,
688
+ }));
689
+
690
+ if (typeof window === 'undefined') return;
691
+
692
+ window.localStorage.setItem(
693
+ getDeadlineSectionCollapsedStorageKey(board.id, section),
694
+ String(collapsed)
695
+ );
696
+ },
697
+ [board.id]
698
+ );
699
+
371
700
  const externalStagingList = useMemo<TaskList | null>(() => {
372
701
  if (!workspace.personal) return null;
373
702
 
@@ -409,10 +738,19 @@ export function BoardViews({
409
738
  : realLists;
410
739
  }, [boardLists, closedTaskListsCollapsed, externalStagingList]);
411
740
 
741
+ const effectiveDeadlineSectionsCollapsed =
742
+ useMemo<KanbanDeadlineCollapsedState>(
743
+ () => ({
744
+ overdue: deadlineSectionsCollapsed.overdue,
745
+ upcoming: deadlineSectionsCollapsed.upcoming,
746
+ }),
747
+ [deadlineSectionsCollapsed.overdue, deadlineSectionsCollapsed.upcoming]
748
+ );
749
+
412
750
  const { data: filteredListCounts, isFetching: isFilteredListCountsFetching } =
413
751
  useQuery({
414
752
  queryKey: ['task-list-counts', board.id, taskFilterKey],
415
- enabled: hasTaskFilters,
753
+ enabled: !localTaskState && hasTaskFilters,
416
754
  queryFn: async () => {
417
755
  const result = await listWorkspaceTasks(effectiveWorkspaceId, {
418
756
  ...taskQueryOptions,
@@ -427,6 +765,31 @@ export function BoardViews({
427
765
  staleTime: 30_000,
428
766
  });
429
767
 
768
+ const locallyFilteredTasks = useMemo(
769
+ () =>
770
+ sortLocalTasks(
771
+ tasks.filter((task) =>
772
+ taskMatchesLocalFilters(task, filters, currentUserId)
773
+ ),
774
+ filters.sortBy
775
+ ),
776
+ [currentUserId, filters, tasks]
777
+ );
778
+
779
+ const localListCounts = useMemo(() => {
780
+ if (!localTaskState || !hasTaskFilters) return null;
781
+
782
+ const counts = new Map<string, number>();
783
+ for (const task of locallyFilteredTasks) {
784
+ counts.set(task.list_id, (counts.get(task.list_id) ?? 0) + 1);
785
+ }
786
+
787
+ return [...counts.entries()].map(([list_id, count]) => ({
788
+ list_id,
789
+ count,
790
+ }));
791
+ }, [hasTaskFilters, localTaskState, locallyFilteredTasks]);
792
+
430
793
  // Filter lists based on selected status filter
431
794
  const statusFilteredLists = useMemo(() => {
432
795
  if (listStatusFilter === 'all') return activeLists;
@@ -439,18 +802,27 @@ export function BoardViews({
439
802
  }, [activeLists, listStatusFilter]);
440
803
 
441
804
  const filteredLists = useMemo(() => {
442
- if (!hasTaskFilters || !filteredListCounts) return statusFilteredLists;
805
+ const listCounts = localTaskState ? localListCounts : filteredListCounts;
806
+ if (!hasTaskFilters || !listCounts) return statusFilteredLists;
443
807
 
444
808
  const countByListId = new Map(
445
- filteredListCounts.map((entry) => [entry.list_id, entry.count] as const)
809
+ listCounts.map((entry) => [entry.list_id, entry.count] as const)
446
810
  );
447
811
 
448
812
  return statusFilteredLists.filter(
449
813
  (list) => (countByListId.get(list.id) ?? 0) > 0
450
814
  );
451
- }, [filteredListCounts, hasTaskFilters, statusFilteredLists]);
815
+ }, [
816
+ filteredListCounts,
817
+ hasTaskFilters,
818
+ localListCounts,
819
+ localTaskState,
820
+ statusFilteredLists,
821
+ ]);
452
822
 
453
823
  const sourceTasks = useMemo(() => {
824
+ if (localTaskState) return locallyFilteredTasks;
825
+
454
826
  if (!shouldEagerLoadTasks) return tasks;
455
827
 
456
828
  if (fullTasks.length === 0) {
@@ -475,7 +847,15 @@ export function BoardViews({
475
847
  }
476
848
 
477
849
  return merged;
478
- }, [fullTasks, hasServerTaskQuery, shouldEagerLoadTasks, sourceScope, tasks]);
850
+ }, [
851
+ fullTasks,
852
+ hasServerTaskQuery,
853
+ localTaskState,
854
+ locallyFilteredTasks,
855
+ shouldEagerLoadTasks,
856
+ sourceScope,
857
+ tasks,
858
+ ]);
479
859
 
480
860
  // Keep only tasks that belong to the server-visible lists/status scope.
481
861
  const filteredTasks = useMemo(() => {
@@ -531,7 +911,11 @@ export function BoardViews({
531
911
  createTask(board.id, targetList.id, selectableLists, filters);
532
912
  },
533
913
  {
534
- enabled: filteredLists.some((list) => !list.is_external_staging),
914
+ enabled:
915
+ !readOnly &&
916
+ currentView !== 'drafts' &&
917
+ currentView !== 'recycle_bin' &&
918
+ filteredLists.some((list) => !list.is_external_staging),
535
919
  ignoreInputs: true,
536
920
  preventDefault: true,
537
921
  }
@@ -543,6 +927,7 @@ export function BoardViews({
543
927
  handleViewChange('kanban');
544
928
  },
545
929
  {
930
+ enabled: viewIsEnabled('kanban'),
546
931
  ignoreInputs: true,
547
932
  preventDefault: true,
548
933
  }
@@ -554,6 +939,19 @@ export function BoardViews({
554
939
  handleViewChange('list');
555
940
  },
556
941
  {
942
+ enabled: viewIsEnabled('list'),
943
+ ignoreInputs: true,
944
+ preventDefault: true,
945
+ }
946
+ );
947
+
948
+ useHotkeySequence(
949
+ HOTKEY_GO_TO_MY_TASKS,
950
+ () => {
951
+ handleViewChange('my_tasks');
952
+ },
953
+ {
954
+ enabled: viewIsEnabled('my_tasks'),
557
955
  ignoreInputs: true,
558
956
  preventDefault: true,
559
957
  }
@@ -565,6 +963,31 @@ export function BoardViews({
565
963
  handleViewChange('timeline');
566
964
  },
567
965
  {
966
+ enabled: viewIsEnabled('timeline'),
967
+ ignoreInputs: true,
968
+ preventDefault: true,
969
+ }
970
+ );
971
+
972
+ useHotkeySequence(
973
+ HOTKEY_GO_TO_DRAFTS,
974
+ () => {
975
+ handleViewChange('drafts');
976
+ },
977
+ {
978
+ enabled: viewIsEnabled('drafts'),
979
+ ignoreInputs: true,
980
+ preventDefault: true,
981
+ }
982
+ );
983
+
984
+ useHotkeySequence(
985
+ HOTKEY_GO_TO_RECYCLE_BIN,
986
+ () => {
987
+ handleViewChange('recycle_bin');
988
+ },
989
+ {
990
+ enabled: viewIsEnabled('recycle_bin'),
568
991
  ignoreInputs: true,
569
992
  preventDefault: true,
570
993
  }
@@ -583,15 +1006,47 @@ export function BoardViews({
583
1006
  lists={filteredLists}
584
1007
  isLoading={false}
585
1008
  disableSort={!!filters.sortBy}
1009
+ deadlineTaskQueryOptions={deadlineTaskQueryOptions}
586
1010
  listStatusFilter={listStatusFilter}
587
1011
  filters={filters}
588
- isMultiSelectMode={isMultiSelectMode}
589
- setIsMultiSelectMode={setIsMultiSelectMode}
1012
+ isMultiSelectMode={readOnly ? false : isMultiSelectMode}
1013
+ setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
590
1014
  onExternalTasksCollapsedChange={handleExternalTasksCollapsedChange}
591
1015
  onTaskListCollapsedChange={handleTaskListCollapsedChange}
1016
+ deadlineSectionsCollapsed={effectiveDeadlineSectionsCollapsed}
1017
+ onDeadlineSectionCollapsedChange={
1018
+ handleDeadlineSectionCollapsedChange
1019
+ }
1020
+ specialTaskListPins={specialTaskListPins}
1021
+ onSpecialTaskListPinnedChange={handleSpecialTaskListPinnedChange}
592
1022
  onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
1023
+ canUseBoardAssignees={boardAssigneesEnabled}
1024
+ assigneeMemberSource={assigneeMemberSource}
1025
+ readOnly={readOnly}
593
1026
  />
594
1027
  );
1028
+ case 'my_tasks':
1029
+ return (
1030
+ <div className="h-full overflow-y-auto p-3 sm:p-4">
1031
+ <div className="mx-auto max-w-5xl pb-20">
1032
+ {currentUserId ? (
1033
+ <MyTasksContent
1034
+ disableAutoCreateBoard
1035
+ embedded
1036
+ initialBoard={{
1037
+ id: board.id,
1038
+ name: board.name ?? null,
1039
+ }}
1040
+ initialLists={boardLists}
1041
+ initialListId={board.default_list_id ?? undefined}
1042
+ isPersonal={workspace.personal}
1043
+ userId={currentUserId}
1044
+ wsId={effectiveWorkspaceId}
1045
+ />
1046
+ ) : null}
1047
+ </div>
1048
+ </div>
1049
+ );
595
1050
  case 'list':
596
1051
  return (
597
1052
  <ListView
@@ -600,8 +1055,11 @@ export function BoardViews({
600
1055
  tasks={effectiveTasks}
601
1056
  lists={filteredLists}
602
1057
  isPersonalWorkspace={workspace.personal}
1058
+ canUseBoardAssignees={boardAssigneesEnabled}
1059
+ assigneeMemberSource={assigneeMemberSource}
603
1060
  preserveTaskOrder={!!filters.sortBy}
604
1061
  searchQuery={filters.searchQuery}
1062
+ readOnly={readOnly}
605
1063
  />
606
1064
  );
607
1065
  case 'timeline':
@@ -611,9 +1069,72 @@ export function BoardViews({
611
1069
  boardId={board.id}
612
1070
  tasks={effectiveTasks}
613
1071
  lists={filteredLists}
1072
+ isPersonalWorkspace={workspace.personal}
1073
+ canUseBoardAssignees={boardAssigneesEnabled}
1074
+ assigneeMemberSource={assigneeMemberSource}
614
1075
  onTaskPartialUpdate={handleTaskPartialUpdate}
615
1076
  />
616
1077
  );
1078
+ case 'drafts':
1079
+ return (
1080
+ <div className="h-full overflow-y-auto p-3 sm:p-4">
1081
+ <DraftsPage
1082
+ boardId={board.id}
1083
+ includeUnassignedForBoard
1084
+ wsId={effectiveWorkspaceId}
1085
+ />
1086
+ </div>
1087
+ );
1088
+ case 'recycle_bin':
1089
+ return (
1090
+ <RecycleBinContent
1091
+ active
1092
+ boardId={board.id}
1093
+ className="h-full"
1094
+ lists={boardLists}
1095
+ translations={{
1096
+ recycleBin: t('recycle_bin'),
1097
+ recycleBinDescription: t('recycle_bin_description'),
1098
+ noDeletedTasks: t('no_deleted_tasks'),
1099
+ deletedTasksWillAppearHere: t('deleted_tasks_will_appear_here'),
1100
+ selectedOfTotal: t('selected_of_total', {
1101
+ selected: '{selected}',
1102
+ total: '{total}',
1103
+ }),
1104
+ deletedTasksCount: t('deleted_tasks_count', { count: '{count}' }),
1105
+ restore: t('restore'),
1106
+ delete: t('delete'),
1107
+ restoreTasksTitle: t('restore_tasks_title', { count: '{count}' }),
1108
+ restoreTasksDescription: t('restore_tasks_description'),
1109
+ cancel: t('cancel'),
1110
+ restoring: t('restoring'),
1111
+ permanentlyDeleteTitle: t('permanently_delete_title', {
1112
+ count: '{count}',
1113
+ }),
1114
+ permanentlyDeleteDescription: t('permanently_delete_description'),
1115
+ deleting: t('deleting'),
1116
+ deletePermanently: t('delete_permanently'),
1117
+ noListsAvailable: t('no_lists_available'),
1118
+ restoredTasks: t('restored_tasks', { count: '{count}' }),
1119
+ failedToRestore: t('failed_to_restore'),
1120
+ permanentlyDeleted: t('permanently_deleted', {
1121
+ count: '{count}',
1122
+ }),
1123
+ failedToDelete: t('failed_to_delete'),
1124
+ deletedAgo: t('deleted_ago', { time: '{time}' }),
1125
+ fromList: t('from_list', { list: '{list}' }),
1126
+ nProjects: t('n_projects', { count: '{count}' }),
1127
+ selectAllTasks: t('select_all_tasks'),
1128
+ selectTask: t('select_task', { name: '{name}' }),
1129
+ critical: tBoards('dialog.priority.critical'),
1130
+ high: tBoards('dialog.priority.high'),
1131
+ normal: tBoards('dialog.priority.normal'),
1132
+ low: tBoards('dialog.priority.low'),
1133
+ unknownList: t('unknown_list'),
1134
+ }}
1135
+ wsId={effectiveWorkspaceId}
1136
+ />
1137
+ );
617
1138
  default:
618
1139
  return (
619
1140
  <KanbanBoard
@@ -625,19 +1146,30 @@ export function BoardViews({
625
1146
  lists={filteredLists}
626
1147
  isLoading={false}
627
1148
  disableSort={!!filters.sortBy}
1149
+ deadlineTaskQueryOptions={deadlineTaskQueryOptions}
628
1150
  listStatusFilter={listStatusFilter}
629
1151
  filters={filters}
630
- isMultiSelectMode={isMultiSelectMode}
631
- setIsMultiSelectMode={setIsMultiSelectMode}
1152
+ isMultiSelectMode={readOnly ? false : isMultiSelectMode}
1153
+ setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
632
1154
  onExternalTasksCollapsedChange={handleExternalTasksCollapsedChange}
633
1155
  onTaskListCollapsedChange={handleTaskListCollapsedChange}
1156
+ deadlineSectionsCollapsed={effectiveDeadlineSectionsCollapsed}
1157
+ onDeadlineSectionCollapsedChange={
1158
+ handleDeadlineSectionCollapsedChange
1159
+ }
1160
+ specialTaskListPins={specialTaskListPins}
1161
+ onSpecialTaskListPinnedChange={handleSpecialTaskListPinnedChange}
634
1162
  onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
1163
+ canUseBoardAssignees={boardAssigneesEnabled}
1164
+ assigneeMemberSource={assigneeMemberSource}
1165
+ readOnly={readOnly}
635
1166
  />
636
1167
  );
637
1168
  }
638
1169
  };
639
1170
 
640
1171
  const showIdleBottomIsland =
1172
+ !readOnly &&
641
1173
  !!idleBottomIsland &&
642
1174
  (currentView !== 'kanban' || !kanbanBulkSelectionActive);
643
1175
 
@@ -655,62 +1187,22 @@ export function BoardViews({
655
1187
  listStatusFilter={listStatusFilter}
656
1188
  onListStatusFilterChange={setListStatusFilter}
657
1189
  isPersonalWorkspace={workspace.personal}
658
- isSearching={isFullTasksFetching || isFilteredListCountsFetching}
1190
+ isSearching={
1191
+ !localTaskState &&
1192
+ (isFullTasksFetching || isFilteredListCountsFetching)
1193
+ }
659
1194
  lists={boardLists}
660
1195
  onUpdate={handleUpdate}
661
- onRecycleBinOpen={() => setRecycleBinOpen(true)}
662
- isMultiSelectMode={isMultiSelectMode}
663
- setIsMultiSelectMode={setIsMultiSelectMode}
664
- hideActions={!canManageBoard}
1196
+ isMultiSelectMode={readOnly ? false : isMultiSelectMode}
1197
+ setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
1198
+ availableViews={enabledViews ?? undefined}
1199
+ hideActions={!canManageBoard || readOnly}
1200
+ publicView={publicView}
1201
+ readOnly={readOnly}
1202
+ titlePrefix={publicHeaderPrefix}
665
1203
  />
666
1204
  <div className="h-full overflow-hidden">{renderView()}</div>
667
1205
  {showIdleBottomIsland ? idleBottomIsland : null}
668
-
669
- <RecycleBinPanel
670
- open={recycleBinOpen}
671
- onOpenChange={setRecycleBinOpen}
672
- wsId={effectiveWorkspaceId}
673
- boardId={board.id}
674
- lists={boardLists}
675
- translations={{
676
- recycleBin: t('recycle_bin'),
677
- recycleBinDescription: t('recycle_bin_description'),
678
- noDeletedTasks: t('no_deleted_tasks'),
679
- deletedTasksWillAppearHere: t('deleted_tasks_will_appear_here'),
680
- selectedOfTotal: t('selected_of_total', {
681
- selected: '{selected}',
682
- total: '{total}',
683
- }),
684
- deletedTasksCount: t('deleted_tasks_count', { count: '{count}' }),
685
- restore: t('restore'),
686
- delete: t('delete'),
687
- restoreTasksTitle: t('restore_tasks_title', { count: '{count}' }),
688
- restoreTasksDescription: t('restore_tasks_description'),
689
- cancel: t('cancel'),
690
- restoring: t('restoring'),
691
- permanentlyDeleteTitle: t('permanently_delete_title', {
692
- count: '{count}',
693
- }),
694
- permanentlyDeleteDescription: t('permanently_delete_description'),
695
- deleting: t('deleting'),
696
- deletePermanently: t('delete_permanently'),
697
- noListsAvailable: t('no_lists_available'),
698
- restoredTasks: t('restored_tasks', { count: '{count}' }),
699
- failedToRestore: t('failed_to_restore'),
700
- permanentlyDeleted: t('permanently_deleted', { count: '{count}' }),
701
- failedToDelete: t('failed_to_delete'),
702
- deletedAgo: t('deleted_ago', { time: '{time}' }),
703
- fromList: t('from_list', { list: '{list}' }),
704
- nProjects: t('n_projects', { count: '{count}' }),
705
- selectAllTasks: t('select_all_tasks'),
706
- selectTask: t('select_task', { name: '{name}' }),
707
- critical: tBoards('dialog.priority.critical'),
708
- high: tBoards('dialog.priority.high'),
709
- normal: tBoards('dialog.priority.normal'),
710
- low: tBoards('dialog.priority.low'),
711
- unknownList: t('unknown_list'),
712
- }}
713
- />
714
1206
  </div>
715
1207
  );
716
1208
  }