@tuturuuu/ui 0.7.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 (226) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/biome.json +1 -1
  3. package/package.json +75 -73
  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/currency-input.test.tsx +43 -0
  29. package/src/components/ui/currency-input.tsx +1 -1
  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 +19 -0
  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/settings-dialog-shell.tsx +2 -1
  40. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  41. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  42. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  43. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  44. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  45. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  46. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  47. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  48. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  49. package/src/components/ui/custom/workspace-select.tsx +8 -3
  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 +3 -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-card.tsx +21 -9
  69. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  70. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  71. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  72. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  73. package/src/components/ui/finance/wallets/form.tsx +15 -4
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  75. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  76. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  77. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  78. package/src/components/ui/input-otp.tsx +1 -1
  79. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  80. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  81. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  82. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  83. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  84. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  85. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  86. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  87. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  88. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  89. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  90. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  91. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  92. package/src/components/ui/money-input.test.tsx +64 -0
  93. package/src/components/ui/money-input.tsx +63 -0
  94. package/src/components/ui/navigation-menu.tsx +1 -1
  95. package/src/components/ui/pagination.tsx +1 -1
  96. package/src/components/ui/radio-group.tsx +1 -1
  97. package/src/components/ui/select.tsx +5 -1
  98. package/src/components/ui/sheet.tsx +1 -1
  99. package/src/components/ui/sidebar.tsx +1 -1
  100. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  101. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  102. package/src/components/ui/storefront/cart-summary.tsx +104 -80
  103. package/src/components/ui/storefront/checkout-overlay.tsx +26 -0
  104. package/src/components/ui/storefront/hero-panel.tsx +2 -8
  105. package/src/components/ui/storefront/image-panel.tsx +6 -0
  106. package/src/components/ui/storefront/index.ts +11 -0
  107. package/src/components/ui/storefront/listing-card.tsx +84 -22
  108. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  109. package/src/components/ui/storefront/product-detail.tsx +289 -0
  110. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  111. package/src/components/ui/storefront/storefront-surface.test.tsx +221 -3
  112. package/src/components/ui/storefront/storefront-surface.tsx +288 -153
  113. package/src/components/ui/storefront/types.ts +27 -1
  114. package/src/components/ui/storefront/utils.ts +117 -27
  115. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  116. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  117. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  118. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  119. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  120. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  121. package/src/components/ui/text-editor/content-migration.ts +41 -18
  122. package/src/components/ui/text-editor/editor.tsx +69 -14
  123. package/src/components/ui/text-editor/extensions.ts +9 -3
  124. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  125. package/src/components/ui/text-editor/image-extension.ts +40 -18
  126. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  127. package/src/components/ui/text-editor/video-extension.ts +11 -2
  128. package/src/components/ui/toast.tsx +1 -1
  129. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  130. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  131. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  132. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  133. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  134. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +113 -46
  135. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  136. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  139. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  141. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +51 -9
  142. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  143. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +127 -38
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  148. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  149. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  150. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  151. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  152. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  153. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  154. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  155. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  156. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  157. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +410 -4
  158. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +106 -14
  159. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  160. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  161. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  162. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +186 -0
  163. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +59 -2
  164. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  165. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  166. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  167. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  168. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  169. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  170. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  171. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  172. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  173. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  174. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  175. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  176. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  177. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  178. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  179. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  180. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  181. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  182. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  183. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  184. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  185. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  186. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +237 -3
  187. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  188. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  189. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  190. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  191. package/src/components/ui/tu-do/shared/board-header.tsx +465 -937
  192. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  193. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  194. package/src/components/ui/tu-do/shared/board-views.tsx +596 -82
  195. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  196. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  197. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  198. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  199. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  200. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  201. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  202. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  203. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  204. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  205. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  206. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  210. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +44 -15
  211. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  212. package/src/declarations.d.ts +1 -0
  213. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  214. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  215. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  216. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  217. package/src/hooks/use-calendar-sync.tsx +247 -243
  218. package/src/hooks/use-calendar.tsx +323 -138
  219. package/src/hooks/use-task-actions.ts +24 -0
  220. package/src/hooks/use-user-workspace-config.ts +75 -0
  221. package/src/hooks/use-workspace-currency.ts +8 -3
  222. package/src/hooks/useBoardRealtime.ts +6 -3
  223. package/src/hooks/useBoardRealtime.types.ts +11 -0
  224. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
  225. package/src/hooks/useCursorTracking.ts +91 -27
  226. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -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: [],
@@ -78,6 +109,140 @@ function getClosedTaskListCollapsedStorageKey(boardId: string, listId: string) {
78
109
  return `${CLOSED_TASK_LIST_COLLAPSED_STORAGE_PREFIX}:${boardId}:${listId}`;
79
110
  }
80
111
 
112
+ function getDeadlineSectionCollapsedStorageKey(
113
+ boardId: string,
114
+ section: KanbanDeadlineSection
115
+ ) {
116
+ return `${DEADLINE_SECTION_COLLAPSED_STORAGE_PREFIX}:${boardId}:${section}`;
117
+ }
118
+
119
+ function taskMatchesLocalFilters(
120
+ task: Task,
121
+ filters: TaskFilters,
122
+ currentUserId?: string
123
+ ) {
124
+ const query = filters.searchQuery?.trim().toLowerCase();
125
+ if (query) {
126
+ const searchableText = [
127
+ task.name,
128
+ task.display_number ? String(task.display_number) : null,
129
+ ...(task.labels ?? []).map((label) => label.name),
130
+ ...(task.projects ?? []).map((project) => project.name),
131
+ ...(task.assignees ?? []).map(
132
+ (assignee) => assignee.display_name ?? assignee.email ?? assignee.handle
133
+ ),
134
+ ]
135
+ .filter(Boolean)
136
+ .join(' ')
137
+ .toLowerCase();
138
+
139
+ if (!searchableText.includes(query)) return false;
140
+ }
141
+
142
+ if (
143
+ filters.labels.length > 0 &&
144
+ !filters.labels.every((label) =>
145
+ task.labels?.some((taskLabel) => taskLabel.id === label.id)
146
+ )
147
+ ) {
148
+ return false;
149
+ }
150
+
151
+ if (
152
+ filters.projects.length > 0 &&
153
+ !filters.projects.every((project) =>
154
+ task.projects?.some((taskProject) => taskProject.id === project.id)
155
+ )
156
+ ) {
157
+ return false;
158
+ }
159
+
160
+ if (
161
+ filters.priorities.length > 0 &&
162
+ (!task.priority || !filters.priorities.includes(task.priority))
163
+ ) {
164
+ return false;
165
+ }
166
+
167
+ if (filters.assignees.length > 0) {
168
+ const assigneeIds = new Set(
169
+ filters.assignees.map((assignee) => assignee.id)
170
+ );
171
+ if (!task.assignees?.some((assignee) => assigneeIds.has(assignee.id))) {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ if (
177
+ filters.includeMyTasks &&
178
+ currentUserId &&
179
+ !task.assignees?.some((assignee) => assignee.id === currentUserId)
180
+ ) {
181
+ return false;
182
+ }
183
+
184
+ if (filters.includeUnassigned && (task.assignees?.length ?? 0) > 0) {
185
+ return false;
186
+ }
187
+
188
+ if (filters.dueDateRange?.from || filters.dueDateRange?.to) {
189
+ if (!task.end_date) return false;
190
+ const dueTime = new Date(task.end_date).getTime();
191
+ const fromTime = filters.dueDateRange.from?.getTime() ?? -Infinity;
192
+ const toTime = filters.dueDateRange.to?.getTime() ?? Infinity;
193
+ if (dueTime < fromTime || dueTime > toTime) return false;
194
+ }
195
+
196
+ if (
197
+ typeof filters.estimationRange?.min === 'number' ||
198
+ typeof filters.estimationRange?.max === 'number'
199
+ ) {
200
+ const estimate = task.estimation_points ?? 0;
201
+ const min = filters.estimationRange.min ?? -Infinity;
202
+ const max = filters.estimationRange.max ?? Infinity;
203
+ if (estimate < min || estimate > max) return false;
204
+ }
205
+
206
+ return true;
207
+ }
208
+
209
+ function getTaskTimestamp(value: string | null | undefined) {
210
+ if (!value) return Number.MAX_SAFE_INTEGER;
211
+ const time = new Date(value).getTime();
212
+ return Number.isFinite(time) ? time : Number.MAX_SAFE_INTEGER;
213
+ }
214
+
215
+ function sortLocalTasks(tasks: Task[], sortBy: TaskFilters['sortBy']) {
216
+ if (!sortBy) return tasks;
217
+
218
+ return [...tasks].sort((a, b) => {
219
+ switch (sortBy) {
220
+ case 'name-asc':
221
+ return a.name.localeCompare(b.name);
222
+ case 'name-desc':
223
+ return b.name.localeCompare(a.name);
224
+ case 'priority-high':
225
+ return priorityCompare(a.priority ?? null, b.priority ?? null);
226
+ case 'priority-low':
227
+ return priorityCompare(b.priority ?? null, a.priority ?? null);
228
+ case 'due-date-asc':
229
+ return getTaskTimestamp(a.end_date) - getTaskTimestamp(b.end_date);
230
+ case 'due-date-desc':
231
+ return getTaskTimestamp(b.end_date) - getTaskTimestamp(a.end_date);
232
+ case 'created-date-asc':
233
+ return getTaskTimestamp(a.created_at) - getTaskTimestamp(b.created_at);
234
+ case 'created-date-desc':
235
+ return getTaskTimestamp(b.created_at) - getTaskTimestamp(a.created_at);
236
+ case 'estimation-high':
237
+ return (b.estimation_points ?? 0) - (a.estimation_points ?? 0);
238
+ case 'estimation-low':
239
+ return (a.estimation_points ?? 0) - (b.estimation_points ?? 0);
240
+ default:
241
+ return 0;
242
+ }
243
+ });
244
+ }
245
+
81
246
  interface Props {
82
247
  workspace: Workspace;
83
248
  workspaceTier?: WorkspaceProductTier | null;
@@ -85,9 +250,13 @@ interface Props {
85
250
  tasks: Task[];
86
251
  lists: TaskList[];
87
252
  workspaceLabels: WorkspaceLabel[];
253
+ availableViews?: ViewType[];
88
254
  canManageBoard?: boolean;
89
255
  currentUserId?: string;
90
256
  idleBottomIsland?: ReactNode;
257
+ publicHeaderPrefix?: ReactNode;
258
+ publicView?: boolean;
259
+ readOnly?: boolean;
91
260
  }
92
261
 
93
262
  export function BoardViews({
@@ -96,9 +265,13 @@ export function BoardViews({
96
265
  board,
97
266
  tasks,
98
267
  lists,
268
+ availableViews,
99
269
  canManageBoard = true,
100
270
  currentUserId,
101
271
  idleBottomIsland,
272
+ publicHeaderPrefix,
273
+ publicView = false,
274
+ readOnly = false,
102
275
  }: Props) {
103
276
  const t = useTranslations('common');
104
277
  const tTasks = useTranslations('ws-tasks');
@@ -110,6 +283,8 @@ export function BoardViews({
110
283
  const [closedTaskListsCollapsed, setClosedTaskListsCollapsed] = useState<
111
284
  Record<string, boolean>
112
285
  >({});
286
+ const [deadlineSectionsCollapsed, setDeadlineSectionsCollapsed] =
287
+ useState<KanbanDeadlineCollapsedState>({});
113
288
  const [filters, setFilters] = useState<TaskFilters>(DEFAULT_TASK_FILTERS);
114
289
  const [listStatusFilter, setListStatusFilter] =
115
290
  useState<ListStatusFilter>('all');
@@ -117,11 +292,58 @@ export function BoardViews({
117
292
  const [taskOverrides, setTaskOverrides] = useState<
118
293
  Record<string, Partial<Task>>
119
294
  >({});
120
- const [recycleBinOpen, setRecycleBinOpen] = useState(false);
121
295
  const [isMultiSelectMode, setIsMultiSelectMode] = useState(false);
122
296
  const [kanbanBulkSelectionActive, setKanbanBulkSelectionActive] =
123
297
  useState(false);
124
298
  const { createTask } = useTaskDialog();
299
+ const localTaskState = readOnly || publicView;
300
+ const { data: pinnedSpecialListsRaw } = useUserWorkspaceConfig(
301
+ effectiveWorkspaceId,
302
+ TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
303
+ null,
304
+ { enabled: !localTaskState }
305
+ );
306
+ const updateUserWorkspaceConfig = useUpdateUserWorkspaceConfig();
307
+ const specialTaskListPins = useMemo(
308
+ () => parseSpecialTaskListPins(pinnedSpecialListsRaw),
309
+ [pinnedSpecialListsRaw]
310
+ );
311
+ const handleSpecialTaskListPinnedChange = useCallback(
312
+ (pin: SpecialTaskListPin, pinned: boolean) => {
313
+ const nextPins = {
314
+ ...specialTaskListPins,
315
+ [pin]: pinned,
316
+ };
317
+
318
+ if (!pinned) delete nextPins[pin];
319
+
320
+ updateUserWorkspaceConfig.mutate({
321
+ configId: TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
322
+ value: serializeSpecialTaskListPins(nextPins),
323
+ workspaceId: effectiveWorkspaceId,
324
+ });
325
+ },
326
+ [effectiveWorkspaceId, specialTaskListPins, updateUserWorkspaceConfig]
327
+ );
328
+ const enabledViews = useMemo(
329
+ () =>
330
+ availableViews ??
331
+ (publicView || readOnly
332
+ ? (['kanban', 'list', 'timeline'] as ViewType[])
333
+ : ([
334
+ 'kanban',
335
+ 'list',
336
+ 'my_tasks',
337
+ 'timeline',
338
+ 'drafts',
339
+ 'recycle_bin',
340
+ ] as ViewType[])),
341
+ [availableViews, publicView, readOnly]
342
+ );
343
+ const viewIsEnabled = useCallback(
344
+ (view: ViewType) => !enabledViews || enabledViews.includes(view),
345
+ [enabledViews]
346
+ );
125
347
  const sourceScope = filters.sourceScope ?? 'all_visible';
126
348
  const sourceWorkspaceIds = filters.sourceWorkspaceIds ?? [];
127
349
  const sourceBoardIds = filters.sourceBoardIds ?? [];
@@ -206,11 +428,21 @@ export function BoardViews({
206
428
  taskQueryOptions,
207
429
  ]
208
430
  );
431
+ const deadlineTaskQueryOptions = useMemo<ListWorkspaceTasksOptions>(() => {
432
+ const { sortBy: _sortBy, ...filterOptions } = taskQueryOptions;
433
+ return {
434
+ ...filterOptions,
435
+ listStatuses: listStatusesForQuery,
436
+ };
437
+ }, [listStatusesForQuery, taskQueryOptions]);
209
438
  const viewHotkeyLabels = useMemo(
210
439
  () => ({
211
440
  kanban: formatHotkeySequence(HOTKEY_GO_TO_KANBAN),
212
441
  list: formatHotkeySequence(HOTKEY_GO_TO_LIST),
442
+ my_tasks: formatHotkeySequence(HOTKEY_GO_TO_MY_TASKS),
213
443
  timeline: formatHotkeySequence(HOTKEY_GO_TO_TIMELINE),
444
+ drafts: formatHotkeySequence(HOTKEY_GO_TO_DRAFTS),
445
+ recycle_bin: formatHotkeySequence(HOTKEY_GO_TO_RECYCLE_BIN),
214
446
  }),
215
447
  []
216
448
  );
@@ -228,6 +460,7 @@ export function BoardViews({
228
460
 
229
461
  const primeFullTaskCache = useCallback(
230
462
  (nextView: ViewType) => {
463
+ if (localTaskState) return;
231
464
  if (nextView !== 'list' && nextView !== 'timeline') return;
232
465
 
233
466
  void queryClient.prefetchQuery({
@@ -236,20 +469,52 @@ export function BoardViews({
236
469
  staleTime: 0,
237
470
  });
238
471
  },
239
- [board.id, fetchBoardTasks, queryClient, taskFilterKey]
472
+ [board.id, fetchBoardTasks, localTaskState, queryClient, taskFilterKey]
240
473
  );
241
474
 
242
475
  const handleViewChange = useCallback(
243
476
  (nextView: ViewType) => {
477
+ if (!viewIsEnabled(nextView)) return;
244
478
  setCurrentView(nextView);
245
479
  primeFullTaskCache(nextView);
480
+ if (!localTaskState) {
481
+ updateUserWorkspaceConfig.mutate({
482
+ configId: TASK_LAST_BOARD_VIEW_CONFIG_ID,
483
+ value: nextView,
484
+ workspaceId: effectiveWorkspaceId,
485
+ });
486
+ }
487
+
488
+ if (typeof window !== 'undefined') {
489
+ const currentUrl = new URL(window.location.href);
490
+ if (!currentUrl.pathname.includes('/tasks/boards/')) return;
491
+
492
+ const params = currentUrl.searchParams;
493
+ if (nextView === 'kanban') {
494
+ params.delete('view');
495
+ } else {
496
+ params.set('view', nextView);
497
+ }
498
+ const nextQuery = params.toString();
499
+ window.history.replaceState(
500
+ window.history.state,
501
+ '',
502
+ `${currentUrl.pathname}${nextQuery ? `?${nextQuery}` : ''}${currentUrl.hash}`
503
+ );
504
+ }
246
505
  },
247
- [primeFullTaskCache]
506
+ [
507
+ effectiveWorkspaceId,
508
+ localTaskState,
509
+ primeFullTaskCache,
510
+ updateUserWorkspaceConfig,
511
+ viewIsEnabled,
512
+ ]
248
513
  );
249
514
 
250
515
  const { data: fullTasks = [], isFetching: isFullTasksFetching } = useQuery({
251
516
  queryKey: ['tasks-full', board.id, taskFilterKey],
252
- enabled: shouldEagerLoadTasks,
517
+ enabled: !localTaskState && shouldEagerLoadTasks,
253
518
  queryFn: fetchBoardTasks,
254
519
  refetchOnMount: 'always',
255
520
  staleTime: 0,
@@ -273,21 +538,36 @@ export function BoardViews({
273
538
 
274
539
  useLayoutEffect(() => {
275
540
  const savedConfig = loadBoardConfig(board.id);
541
+ const requestedView =
542
+ typeof window === 'undefined' ||
543
+ !window.location.pathname.includes('/tasks/boards/')
544
+ ? null
545
+ : (new URLSearchParams(window.location.search).get(
546
+ 'view'
547
+ ) as ViewType | null);
548
+ const defaultView = enabledViews?.[0] ?? 'kanban';
549
+ const initialView =
550
+ requestedView && viewIsEnabled(requestedView) ? requestedView : null;
276
551
 
277
552
  if (!savedConfig) {
278
- setCurrentView('kanban');
553
+ setCurrentView(initialView ?? defaultView);
279
554
  setFilters(DEFAULT_TASK_FILTERS);
280
555
  setListStatusFilter('all');
281
556
  return;
282
557
  }
283
558
 
284
- setCurrentView(savedConfig.currentView);
559
+ setCurrentView(
560
+ initialView ??
561
+ (viewIsEnabled(savedConfig.currentView)
562
+ ? savedConfig.currentView
563
+ : defaultView)
564
+ );
285
565
  setFilters({
286
566
  ...DEFAULT_TASK_FILTERS,
287
567
  ...savedConfig.filters,
288
568
  });
289
569
  setListStatusFilter(savedConfig.listStatusFilter);
290
- }, [board.id]);
570
+ }, [board.id, enabledViews, viewIsEnabled]);
291
571
 
292
572
  useEffect(() => {
293
573
  if (!workspace.personal || typeof window === 'undefined') {
@@ -308,6 +588,8 @@ export function BoardViews({
308
588
 
309
589
  const handleExternalTasksCollapsedChange = useCallback(
310
590
  (collapsed: boolean) => {
591
+ if (collapsed && specialTaskListPins.external_tasks) return;
592
+
311
593
  setExternalTasksCollapsed(collapsed);
312
594
 
313
595
  if (!workspace.personal || typeof window === 'undefined') return;
@@ -317,7 +599,7 @@ export function BoardViews({
317
599
  String(collapsed)
318
600
  );
319
601
  },
320
- [board.id, workspace.personal]
602
+ [board.id, specialTaskListPins.external_tasks, workspace.personal]
321
603
  );
322
604
 
323
605
  useEffect(() => {
@@ -353,6 +635,16 @@ export function BoardViews({
353
635
 
354
636
  const handleTaskListCollapsedChange = useCallback(
355
637
  (listId: string, collapsed: boolean) => {
638
+ if (
639
+ collapsed &&
640
+ specialTaskListPins.closed_tasks &&
641
+ boardLists.some(
642
+ (list) => list.id === listId && list.status === 'closed'
643
+ )
644
+ ) {
645
+ return;
646
+ }
647
+
356
648
  setClosedTaskListsCollapsed((previous) => ({
357
649
  ...previous,
358
650
  [listId]: collapsed,
@@ -365,7 +657,54 @@ export function BoardViews({
365
657
  String(collapsed)
366
658
  );
367
659
  },
368
- [board.id]
660
+ [board.id, boardLists, specialTaskListPins.closed_tasks]
661
+ );
662
+
663
+ useEffect(() => {
664
+ setDeadlineSectionsCollapsed((previous) => {
665
+ const next: KanbanDeadlineCollapsedState = {};
666
+
667
+ for (const section of ['overdue', 'upcoming'] as const) {
668
+ const storedValue =
669
+ typeof window === 'undefined'
670
+ ? null
671
+ : window.localStorage.getItem(
672
+ getDeadlineSectionCollapsedStorageKey(board.id, section)
673
+ );
674
+
675
+ next[section] =
676
+ storedValue === null
677
+ ? (previous[section] ?? false)
678
+ : storedValue === 'true';
679
+ }
680
+
681
+ return next;
682
+ });
683
+ }, [board.id]);
684
+
685
+ const handleDeadlineSectionCollapsedChange = useCallback(
686
+ (section: KanbanDeadlineSection, collapsed: boolean) => {
687
+ if (
688
+ collapsed &&
689
+ ((section === 'overdue' && specialTaskListPins.overdue) ||
690
+ (section === 'upcoming' && specialTaskListPins.upcoming))
691
+ ) {
692
+ return;
693
+ }
694
+
695
+ setDeadlineSectionsCollapsed((previous) => ({
696
+ ...previous,
697
+ [section]: collapsed,
698
+ }));
699
+
700
+ if (typeof window === 'undefined') return;
701
+
702
+ window.localStorage.setItem(
703
+ getDeadlineSectionCollapsedStorageKey(board.id, section),
704
+ String(collapsed)
705
+ );
706
+ },
707
+ [board.id, specialTaskListPins.overdue, specialTaskListPins.upcoming]
369
708
  );
370
709
 
371
710
  const externalStagingList = useMemo<TaskList | null>(() => {
@@ -383,12 +722,15 @@ export function BoardViews({
383
722
  color: 'CYAN',
384
723
  position: Number.MIN_SAFE_INTEGER,
385
724
  is_external_staging: true,
386
- is_external_collapsed: externalTasksCollapsed,
725
+ is_external_collapsed: specialTaskListPins.external_tasks
726
+ ? false
727
+ : externalTasksCollapsed,
387
728
  };
388
729
  }, [
389
730
  board.created_at,
390
731
  board.id,
391
732
  externalTasksCollapsed,
733
+ specialTaskListPins.external_tasks,
392
734
  tTasks,
393
735
  workspace.personal,
394
736
  ]);
@@ -400,19 +742,44 @@ export function BoardViews({
400
742
  list.status === 'closed'
401
743
  ? {
402
744
  ...list,
403
- is_collapsed: closedTaskListsCollapsed[list.id] ?? true,
745
+ is_collapsed: specialTaskListPins.closed_tasks
746
+ ? false
747
+ : (closedTaskListsCollapsed[list.id] ?? true),
404
748
  }
405
749
  : list
406
750
  );
407
751
  return externalStagingList
408
752
  ? [externalStagingList, ...realLists]
409
753
  : realLists;
410
- }, [boardLists, closedTaskListsCollapsed, externalStagingList]);
754
+ }, [
755
+ boardLists,
756
+ closedTaskListsCollapsed,
757
+ externalStagingList,
758
+ specialTaskListPins.closed_tasks,
759
+ ]);
760
+
761
+ const effectiveDeadlineSectionsCollapsed =
762
+ useMemo<KanbanDeadlineCollapsedState>(
763
+ () => ({
764
+ overdue: specialTaskListPins.overdue
765
+ ? false
766
+ : deadlineSectionsCollapsed.overdue,
767
+ upcoming: specialTaskListPins.upcoming
768
+ ? false
769
+ : deadlineSectionsCollapsed.upcoming,
770
+ }),
771
+ [
772
+ deadlineSectionsCollapsed.overdue,
773
+ deadlineSectionsCollapsed.upcoming,
774
+ specialTaskListPins.overdue,
775
+ specialTaskListPins.upcoming,
776
+ ]
777
+ );
411
778
 
412
779
  const { data: filteredListCounts, isFetching: isFilteredListCountsFetching } =
413
780
  useQuery({
414
781
  queryKey: ['task-list-counts', board.id, taskFilterKey],
415
- enabled: hasTaskFilters,
782
+ enabled: !localTaskState && hasTaskFilters,
416
783
  queryFn: async () => {
417
784
  const result = await listWorkspaceTasks(effectiveWorkspaceId, {
418
785
  ...taskQueryOptions,
@@ -427,6 +794,31 @@ export function BoardViews({
427
794
  staleTime: 30_000,
428
795
  });
429
796
 
797
+ const locallyFilteredTasks = useMemo(
798
+ () =>
799
+ sortLocalTasks(
800
+ tasks.filter((task) =>
801
+ taskMatchesLocalFilters(task, filters, currentUserId)
802
+ ),
803
+ filters.sortBy
804
+ ),
805
+ [currentUserId, filters, tasks]
806
+ );
807
+
808
+ const localListCounts = useMemo(() => {
809
+ if (!localTaskState || !hasTaskFilters) return null;
810
+
811
+ const counts = new Map<string, number>();
812
+ for (const task of locallyFilteredTasks) {
813
+ counts.set(task.list_id, (counts.get(task.list_id) ?? 0) + 1);
814
+ }
815
+
816
+ return [...counts.entries()].map(([list_id, count]) => ({
817
+ list_id,
818
+ count,
819
+ }));
820
+ }, [hasTaskFilters, localTaskState, locallyFilteredTasks]);
821
+
430
822
  // Filter lists based on selected status filter
431
823
  const statusFilteredLists = useMemo(() => {
432
824
  if (listStatusFilter === 'all') return activeLists;
@@ -439,18 +831,27 @@ export function BoardViews({
439
831
  }, [activeLists, listStatusFilter]);
440
832
 
441
833
  const filteredLists = useMemo(() => {
442
- if (!hasTaskFilters || !filteredListCounts) return statusFilteredLists;
834
+ const listCounts = localTaskState ? localListCounts : filteredListCounts;
835
+ if (!hasTaskFilters || !listCounts) return statusFilteredLists;
443
836
 
444
837
  const countByListId = new Map(
445
- filteredListCounts.map((entry) => [entry.list_id, entry.count] as const)
838
+ listCounts.map((entry) => [entry.list_id, entry.count] as const)
446
839
  );
447
840
 
448
841
  return statusFilteredLists.filter(
449
842
  (list) => (countByListId.get(list.id) ?? 0) > 0
450
843
  );
451
- }, [filteredListCounts, hasTaskFilters, statusFilteredLists]);
844
+ }, [
845
+ filteredListCounts,
846
+ hasTaskFilters,
847
+ localListCounts,
848
+ localTaskState,
849
+ statusFilteredLists,
850
+ ]);
452
851
 
453
852
  const sourceTasks = useMemo(() => {
853
+ if (localTaskState) return locallyFilteredTasks;
854
+
454
855
  if (!shouldEagerLoadTasks) return tasks;
455
856
 
456
857
  if (fullTasks.length === 0) {
@@ -475,7 +876,15 @@ export function BoardViews({
475
876
  }
476
877
 
477
878
  return merged;
478
- }, [fullTasks, hasServerTaskQuery, shouldEagerLoadTasks, sourceScope, tasks]);
879
+ }, [
880
+ fullTasks,
881
+ hasServerTaskQuery,
882
+ localTaskState,
883
+ locallyFilteredTasks,
884
+ shouldEagerLoadTasks,
885
+ sourceScope,
886
+ tasks,
887
+ ]);
479
888
 
480
889
  // Keep only tasks that belong to the server-visible lists/status scope.
481
890
  const filteredTasks = useMemo(() => {
@@ -519,17 +928,23 @@ export function BoardViews({
519
928
  useHotkey(
520
929
  HOTKEY_CREATE_TASK,
521
930
  () => {
522
- const firstList = filteredLists.find((list) => !list.is_external_staging);
523
- if (!firstList) return;
524
- createTask(
525
- board.id,
526
- firstList.id,
527
- filteredLists.filter((list) => !list.is_external_staging),
528
- filters
931
+ const selectableLists = filteredLists.filter(
932
+ (list) => !list.is_external_staging
529
933
  );
934
+ // Prefer the board's configured default list for new tasks, falling back
935
+ // to the first selectable list when unset or the list is unavailable.
936
+ const targetList =
937
+ selectableLists.find((list) => list.id === board.default_list_id) ??
938
+ selectableLists[0];
939
+ if (!targetList) return;
940
+ createTask(board.id, targetList.id, selectableLists, filters);
530
941
  },
531
942
  {
532
- enabled: filteredLists.some((list) => !list.is_external_staging),
943
+ enabled:
944
+ !readOnly &&
945
+ currentView !== 'drafts' &&
946
+ currentView !== 'recycle_bin' &&
947
+ filteredLists.some((list) => !list.is_external_staging),
533
948
  ignoreInputs: true,
534
949
  preventDefault: true,
535
950
  }
@@ -541,6 +956,7 @@ export function BoardViews({
541
956
  handleViewChange('kanban');
542
957
  },
543
958
  {
959
+ enabled: viewIsEnabled('kanban'),
544
960
  ignoreInputs: true,
545
961
  preventDefault: true,
546
962
  }
@@ -552,6 +968,19 @@ export function BoardViews({
552
968
  handleViewChange('list');
553
969
  },
554
970
  {
971
+ enabled: viewIsEnabled('list'),
972
+ ignoreInputs: true,
973
+ preventDefault: true,
974
+ }
975
+ );
976
+
977
+ useHotkeySequence(
978
+ HOTKEY_GO_TO_MY_TASKS,
979
+ () => {
980
+ handleViewChange('my_tasks');
981
+ },
982
+ {
983
+ enabled: viewIsEnabled('my_tasks'),
555
984
  ignoreInputs: true,
556
985
  preventDefault: true,
557
986
  }
@@ -563,6 +992,31 @@ export function BoardViews({
563
992
  handleViewChange('timeline');
564
993
  },
565
994
  {
995
+ enabled: viewIsEnabled('timeline'),
996
+ ignoreInputs: true,
997
+ preventDefault: true,
998
+ }
999
+ );
1000
+
1001
+ useHotkeySequence(
1002
+ HOTKEY_GO_TO_DRAFTS,
1003
+ () => {
1004
+ handleViewChange('drafts');
1005
+ },
1006
+ {
1007
+ enabled: viewIsEnabled('drafts'),
1008
+ ignoreInputs: true,
1009
+ preventDefault: true,
1010
+ }
1011
+ );
1012
+
1013
+ useHotkeySequence(
1014
+ HOTKEY_GO_TO_RECYCLE_BIN,
1015
+ () => {
1016
+ handleViewChange('recycle_bin');
1017
+ },
1018
+ {
1019
+ enabled: viewIsEnabled('recycle_bin'),
566
1020
  ignoreInputs: true,
567
1021
  preventDefault: true,
568
1022
  }
@@ -581,15 +1035,45 @@ export function BoardViews({
581
1035
  lists={filteredLists}
582
1036
  isLoading={false}
583
1037
  disableSort={!!filters.sortBy}
1038
+ deadlineTaskQueryOptions={deadlineTaskQueryOptions}
584
1039
  listStatusFilter={listStatusFilter}
585
1040
  filters={filters}
586
- isMultiSelectMode={isMultiSelectMode}
587
- setIsMultiSelectMode={setIsMultiSelectMode}
1041
+ isMultiSelectMode={readOnly ? false : isMultiSelectMode}
1042
+ setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
588
1043
  onExternalTasksCollapsedChange={handleExternalTasksCollapsedChange}
589
1044
  onTaskListCollapsedChange={handleTaskListCollapsedChange}
1045
+ deadlineSectionsCollapsed={effectiveDeadlineSectionsCollapsed}
1046
+ onDeadlineSectionCollapsedChange={
1047
+ handleDeadlineSectionCollapsedChange
1048
+ }
1049
+ specialTaskListPins={specialTaskListPins}
1050
+ onSpecialTaskListPinnedChange={handleSpecialTaskListPinnedChange}
590
1051
  onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
1052
+ readOnly={readOnly}
591
1053
  />
592
1054
  );
1055
+ case 'my_tasks':
1056
+ return (
1057
+ <div className="h-full overflow-y-auto p-3 sm:p-4">
1058
+ <div className="mx-auto max-w-5xl pb-20">
1059
+ {currentUserId ? (
1060
+ <MyTasksContent
1061
+ disableAutoCreateBoard
1062
+ embedded
1063
+ initialBoard={{
1064
+ id: board.id,
1065
+ name: board.name ?? null,
1066
+ }}
1067
+ initialLists={boardLists}
1068
+ initialListId={board.default_list_id ?? undefined}
1069
+ isPersonal={workspace.personal}
1070
+ userId={currentUserId}
1071
+ wsId={effectiveWorkspaceId}
1072
+ />
1073
+ ) : null}
1074
+ </div>
1075
+ </div>
1076
+ );
593
1077
  case 'list':
594
1078
  return (
595
1079
  <ListView
@@ -600,6 +1084,7 @@ export function BoardViews({
600
1084
  isPersonalWorkspace={workspace.personal}
601
1085
  preserveTaskOrder={!!filters.sortBy}
602
1086
  searchQuery={filters.searchQuery}
1087
+ readOnly={readOnly}
603
1088
  />
604
1089
  );
605
1090
  case 'timeline':
@@ -612,6 +1097,66 @@ export function BoardViews({
612
1097
  onTaskPartialUpdate={handleTaskPartialUpdate}
613
1098
  />
614
1099
  );
1100
+ case 'drafts':
1101
+ return (
1102
+ <div className="h-full overflow-y-auto p-3 sm:p-4">
1103
+ <DraftsPage
1104
+ boardId={board.id}
1105
+ includeUnassignedForBoard
1106
+ wsId={effectiveWorkspaceId}
1107
+ />
1108
+ </div>
1109
+ );
1110
+ case 'recycle_bin':
1111
+ return (
1112
+ <RecycleBinContent
1113
+ active
1114
+ boardId={board.id}
1115
+ className="h-full"
1116
+ lists={boardLists}
1117
+ translations={{
1118
+ recycleBin: t('recycle_bin'),
1119
+ recycleBinDescription: t('recycle_bin_description'),
1120
+ noDeletedTasks: t('no_deleted_tasks'),
1121
+ deletedTasksWillAppearHere: t('deleted_tasks_will_appear_here'),
1122
+ selectedOfTotal: t('selected_of_total', {
1123
+ selected: '{selected}',
1124
+ total: '{total}',
1125
+ }),
1126
+ deletedTasksCount: t('deleted_tasks_count', { count: '{count}' }),
1127
+ restore: t('restore'),
1128
+ delete: t('delete'),
1129
+ restoreTasksTitle: t('restore_tasks_title', { count: '{count}' }),
1130
+ restoreTasksDescription: t('restore_tasks_description'),
1131
+ cancel: t('cancel'),
1132
+ restoring: t('restoring'),
1133
+ permanentlyDeleteTitle: t('permanently_delete_title', {
1134
+ count: '{count}',
1135
+ }),
1136
+ permanentlyDeleteDescription: t('permanently_delete_description'),
1137
+ deleting: t('deleting'),
1138
+ deletePermanently: t('delete_permanently'),
1139
+ noListsAvailable: t('no_lists_available'),
1140
+ restoredTasks: t('restored_tasks', { count: '{count}' }),
1141
+ failedToRestore: t('failed_to_restore'),
1142
+ permanentlyDeleted: t('permanently_deleted', {
1143
+ count: '{count}',
1144
+ }),
1145
+ failedToDelete: t('failed_to_delete'),
1146
+ deletedAgo: t('deleted_ago', { time: '{time}' }),
1147
+ fromList: t('from_list', { list: '{list}' }),
1148
+ nProjects: t('n_projects', { count: '{count}' }),
1149
+ selectAllTasks: t('select_all_tasks'),
1150
+ selectTask: t('select_task', { name: '{name}' }),
1151
+ critical: tBoards('dialog.priority.critical'),
1152
+ high: tBoards('dialog.priority.high'),
1153
+ normal: tBoards('dialog.priority.normal'),
1154
+ low: tBoards('dialog.priority.low'),
1155
+ unknownList: t('unknown_list'),
1156
+ }}
1157
+ wsId={effectiveWorkspaceId}
1158
+ />
1159
+ );
615
1160
  default:
616
1161
  return (
617
1162
  <KanbanBoard
@@ -623,19 +1168,28 @@ export function BoardViews({
623
1168
  lists={filteredLists}
624
1169
  isLoading={false}
625
1170
  disableSort={!!filters.sortBy}
1171
+ deadlineTaskQueryOptions={deadlineTaskQueryOptions}
626
1172
  listStatusFilter={listStatusFilter}
627
1173
  filters={filters}
628
- isMultiSelectMode={isMultiSelectMode}
629
- setIsMultiSelectMode={setIsMultiSelectMode}
1174
+ isMultiSelectMode={readOnly ? false : isMultiSelectMode}
1175
+ setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
630
1176
  onExternalTasksCollapsedChange={handleExternalTasksCollapsedChange}
631
1177
  onTaskListCollapsedChange={handleTaskListCollapsedChange}
1178
+ deadlineSectionsCollapsed={effectiveDeadlineSectionsCollapsed}
1179
+ onDeadlineSectionCollapsedChange={
1180
+ handleDeadlineSectionCollapsedChange
1181
+ }
1182
+ specialTaskListPins={specialTaskListPins}
1183
+ onSpecialTaskListPinnedChange={handleSpecialTaskListPinnedChange}
632
1184
  onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
1185
+ readOnly={readOnly}
633
1186
  />
634
1187
  );
635
1188
  }
636
1189
  };
637
1190
 
638
1191
  const showIdleBottomIsland =
1192
+ !readOnly &&
639
1193
  !!idleBottomIsland &&
640
1194
  (currentView !== 'kanban' || !kanbanBulkSelectionActive);
641
1195
 
@@ -653,62 +1207,22 @@ export function BoardViews({
653
1207
  listStatusFilter={listStatusFilter}
654
1208
  onListStatusFilterChange={setListStatusFilter}
655
1209
  isPersonalWorkspace={workspace.personal}
656
- isSearching={isFullTasksFetching || isFilteredListCountsFetching}
1210
+ isSearching={
1211
+ !localTaskState &&
1212
+ (isFullTasksFetching || isFilteredListCountsFetching)
1213
+ }
657
1214
  lists={boardLists}
658
1215
  onUpdate={handleUpdate}
659
- onRecycleBinOpen={() => setRecycleBinOpen(true)}
660
- isMultiSelectMode={isMultiSelectMode}
661
- setIsMultiSelectMode={setIsMultiSelectMode}
662
- hideActions={!canManageBoard}
1216
+ isMultiSelectMode={readOnly ? false : isMultiSelectMode}
1217
+ setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
1218
+ availableViews={enabledViews ?? undefined}
1219
+ hideActions={!canManageBoard || readOnly}
1220
+ publicView={publicView}
1221
+ readOnly={readOnly}
1222
+ titlePrefix={publicHeaderPrefix}
663
1223
  />
664
1224
  <div className="h-full overflow-hidden">{renderView()}</div>
665
1225
  {showIdleBottomIsland ? idleBottomIsland : null}
666
-
667
- <RecycleBinPanel
668
- open={recycleBinOpen}
669
- onOpenChange={setRecycleBinOpen}
670
- wsId={effectiveWorkspaceId}
671
- boardId={board.id}
672
- lists={boardLists}
673
- translations={{
674
- recycleBin: t('recycle_bin'),
675
- recycleBinDescription: t('recycle_bin_description'),
676
- noDeletedTasks: t('no_deleted_tasks'),
677
- deletedTasksWillAppearHere: t('deleted_tasks_will_appear_here'),
678
- selectedOfTotal: t('selected_of_total', {
679
- selected: '{selected}',
680
- total: '{total}',
681
- }),
682
- deletedTasksCount: t('deleted_tasks_count', { count: '{count}' }),
683
- restore: t('restore'),
684
- delete: t('delete'),
685
- restoreTasksTitle: t('restore_tasks_title', { count: '{count}' }),
686
- restoreTasksDescription: t('restore_tasks_description'),
687
- cancel: t('cancel'),
688
- restoring: t('restoring'),
689
- permanentlyDeleteTitle: t('permanently_delete_title', {
690
- count: '{count}',
691
- }),
692
- permanentlyDeleteDescription: t('permanently_delete_description'),
693
- deleting: t('deleting'),
694
- deletePermanently: t('delete_permanently'),
695
- noListsAvailable: t('no_lists_available'),
696
- restoredTasks: t('restored_tasks', { count: '{count}' }),
697
- failedToRestore: t('failed_to_restore'),
698
- permanentlyDeleted: t('permanently_deleted', { count: '{count}' }),
699
- failedToDelete: t('failed_to_delete'),
700
- deletedAgo: t('deleted_ago', { time: '{time}' }),
701
- fromList: t('from_list', { list: '{list}' }),
702
- nProjects: t('n_projects', { count: '{count}' }),
703
- selectAllTasks: t('select_all_tasks'),
704
- selectTask: t('select_task', { name: '{name}' }),
705
- critical: tBoards('dialog.priority.critical'),
706
- high: tBoards('dialog.priority.high'),
707
- normal: tBoards('dialog.priority.normal'),
708
- low: tBoards('dialog.priority.low'),
709
- unknownList: t('unknown_list'),
710
- }}
711
- />
712
1226
  </div>
713
1227
  );
714
1228
  }