@tuturuuu/ui 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/biome.json +1 -1
  3. package/package.json +73 -71
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  29. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  30. package/src/components/ui/custom/combobox.test.tsx +195 -0
  31. package/src/components/ui/custom/combobox.tsx +273 -156
  32. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  33. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  34. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  35. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  36. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  37. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  38. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  39. package/src/components/ui/custom/workspace-select.tsx +8 -3
  40. package/src/components/ui/dialog.test.tsx +52 -0
  41. package/src/components/ui/dialog.tsx +6 -2
  42. package/src/components/ui/dropdown-menu.tsx +5 -1
  43. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  44. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  45. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  46. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  47. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  48. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  49. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  50. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  51. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  52. package/src/components/ui/finance/invoices/utils.ts +3 -1
  53. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  54. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  55. package/src/components/ui/finance/transactions/form.tsx +2 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  57. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  58. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  59. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  60. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  61. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  62. package/src/components/ui/finance/wallets/form.tsx +15 -4
  63. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  64. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  65. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  66. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  67. package/src/components/ui/input-otp.tsx +1 -1
  68. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  69. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  70. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  71. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  72. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  73. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  74. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  75. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  76. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  77. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  78. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  79. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  80. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  81. package/src/components/ui/navigation-menu.tsx +1 -1
  82. package/src/components/ui/pagination.tsx +1 -1
  83. package/src/components/ui/radio-group.tsx +1 -1
  84. package/src/components/ui/select.tsx +5 -1
  85. package/src/components/ui/sheet.tsx +1 -1
  86. package/src/components/ui/sidebar.tsx +1 -1
  87. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  88. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  89. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  90. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  91. package/src/components/ui/storefront/listing-card.tsx +1 -1
  92. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  93. package/src/components/ui/storefront/product-detail.tsx +1 -1
  94. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  95. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  96. package/src/components/ui/storefront/types.ts +4 -0
  97. package/src/components/ui/storefront/utils.ts +6 -0
  98. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  99. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  100. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  101. package/src/components/ui/text-editor/editor.tsx +69 -14
  102. package/src/components/ui/text-editor/extensions.ts +8 -2
  103. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  104. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  105. package/src/components/ui/toast.tsx +1 -1
  106. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  107. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  108. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  109. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
  110. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  111. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  112. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  113. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  114. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  115. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  116. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  117. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  118. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  119. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  120. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  121. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  122. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  127. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  128. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  129. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  131. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
  132. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
  133. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  134. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  135. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  136. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
  137. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
  138. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  139. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  140. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  141. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  142. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  143. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  144. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  145. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  146. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  147. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  148. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  149. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  150. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  151. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  152. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  153. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  154. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
  155. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  156. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  157. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  158. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  159. package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
  160. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  161. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  162. package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
  163. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  164. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  165. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  166. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  167. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  168. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  169. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  170. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  171. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  172. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
  173. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  174. package/src/declarations.d.ts +1 -0
  175. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  176. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  177. package/src/hooks/use-calendar-sync.tsx +247 -243
  178. package/src/hooks/use-calendar.tsx +323 -138
  179. package/src/hooks/use-task-actions.ts +24 -0
  180. package/src/hooks/use-user-workspace-config.ts +75 -0
  181. package/src/hooks/use-workspace-currency.ts +8 -3
  182. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
@@ -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(() => {
@@ -531,7 +940,11 @@ export function BoardViews({
531
940
  createTask(board.id, targetList.id, selectableLists, filters);
532
941
  },
533
942
  {
534
- 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),
535
948
  ignoreInputs: true,
536
949
  preventDefault: true,
537
950
  }
@@ -543,6 +956,7 @@ export function BoardViews({
543
956
  handleViewChange('kanban');
544
957
  },
545
958
  {
959
+ enabled: viewIsEnabled('kanban'),
546
960
  ignoreInputs: true,
547
961
  preventDefault: true,
548
962
  }
@@ -554,6 +968,19 @@ export function BoardViews({
554
968
  handleViewChange('list');
555
969
  },
556
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'),
557
984
  ignoreInputs: true,
558
985
  preventDefault: true,
559
986
  }
@@ -565,6 +992,31 @@ export function BoardViews({
565
992
  handleViewChange('timeline');
566
993
  },
567
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'),
568
1020
  ignoreInputs: true,
569
1021
  preventDefault: true,
570
1022
  }
@@ -583,15 +1035,45 @@ export function BoardViews({
583
1035
  lists={filteredLists}
584
1036
  isLoading={false}
585
1037
  disableSort={!!filters.sortBy}
1038
+ deadlineTaskQueryOptions={deadlineTaskQueryOptions}
586
1039
  listStatusFilter={listStatusFilter}
587
1040
  filters={filters}
588
- isMultiSelectMode={isMultiSelectMode}
589
- setIsMultiSelectMode={setIsMultiSelectMode}
1041
+ isMultiSelectMode={readOnly ? false : isMultiSelectMode}
1042
+ setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
590
1043
  onExternalTasksCollapsedChange={handleExternalTasksCollapsedChange}
591
1044
  onTaskListCollapsedChange={handleTaskListCollapsedChange}
1045
+ deadlineSectionsCollapsed={effectiveDeadlineSectionsCollapsed}
1046
+ onDeadlineSectionCollapsedChange={
1047
+ handleDeadlineSectionCollapsedChange
1048
+ }
1049
+ specialTaskListPins={specialTaskListPins}
1050
+ onSpecialTaskListPinnedChange={handleSpecialTaskListPinnedChange}
592
1051
  onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
1052
+ readOnly={readOnly}
593
1053
  />
594
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
+ );
595
1077
  case 'list':
596
1078
  return (
597
1079
  <ListView
@@ -602,6 +1084,7 @@ export function BoardViews({
602
1084
  isPersonalWorkspace={workspace.personal}
603
1085
  preserveTaskOrder={!!filters.sortBy}
604
1086
  searchQuery={filters.searchQuery}
1087
+ readOnly={readOnly}
605
1088
  />
606
1089
  );
607
1090
  case 'timeline':
@@ -614,6 +1097,66 @@ export function BoardViews({
614
1097
  onTaskPartialUpdate={handleTaskPartialUpdate}
615
1098
  />
616
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
+ );
617
1160
  default:
618
1161
  return (
619
1162
  <KanbanBoard
@@ -625,19 +1168,28 @@ export function BoardViews({
625
1168
  lists={filteredLists}
626
1169
  isLoading={false}
627
1170
  disableSort={!!filters.sortBy}
1171
+ deadlineTaskQueryOptions={deadlineTaskQueryOptions}
628
1172
  listStatusFilter={listStatusFilter}
629
1173
  filters={filters}
630
- isMultiSelectMode={isMultiSelectMode}
631
- setIsMultiSelectMode={setIsMultiSelectMode}
1174
+ isMultiSelectMode={readOnly ? false : isMultiSelectMode}
1175
+ setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
632
1176
  onExternalTasksCollapsedChange={handleExternalTasksCollapsedChange}
633
1177
  onTaskListCollapsedChange={handleTaskListCollapsedChange}
1178
+ deadlineSectionsCollapsed={effectiveDeadlineSectionsCollapsed}
1179
+ onDeadlineSectionCollapsedChange={
1180
+ handleDeadlineSectionCollapsedChange
1181
+ }
1182
+ specialTaskListPins={specialTaskListPins}
1183
+ onSpecialTaskListPinnedChange={handleSpecialTaskListPinnedChange}
634
1184
  onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
1185
+ readOnly={readOnly}
635
1186
  />
636
1187
  );
637
1188
  }
638
1189
  };
639
1190
 
640
1191
  const showIdleBottomIsland =
1192
+ !readOnly &&
641
1193
  !!idleBottomIsland &&
642
1194
  (currentView !== 'kanban' || !kanbanBulkSelectionActive);
643
1195
 
@@ -655,62 +1207,22 @@ export function BoardViews({
655
1207
  listStatusFilter={listStatusFilter}
656
1208
  onListStatusFilterChange={setListStatusFilter}
657
1209
  isPersonalWorkspace={workspace.personal}
658
- isSearching={isFullTasksFetching || isFilteredListCountsFetching}
1210
+ isSearching={
1211
+ !localTaskState &&
1212
+ (isFullTasksFetching || isFilteredListCountsFetching)
1213
+ }
659
1214
  lists={boardLists}
660
1215
  onUpdate={handleUpdate}
661
- onRecycleBinOpen={() => setRecycleBinOpen(true)}
662
- isMultiSelectMode={isMultiSelectMode}
663
- setIsMultiSelectMode={setIsMultiSelectMode}
664
- 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}
665
1223
  />
666
1224
  <div className="h-full overflow-hidden">{renderView()}</div>
667
1225
  {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
1226
  </div>
715
1227
  );
716
1228
  }