@tuturuuu/ui 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/biome.json +1 -1
  3. package/package.json +73 -71
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  29. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  30. package/src/components/ui/custom/combobox.test.tsx +195 -0
  31. package/src/components/ui/custom/combobox.tsx +273 -156
  32. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  33. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  34. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  35. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  36. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  37. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  38. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  39. package/src/components/ui/custom/workspace-select.tsx +8 -3
  40. package/src/components/ui/dialog.test.tsx +52 -0
  41. package/src/components/ui/dialog.tsx +6 -2
  42. package/src/components/ui/dropdown-menu.tsx +5 -1
  43. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  44. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  45. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  46. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  47. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  48. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  49. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  50. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  51. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  52. package/src/components/ui/finance/invoices/utils.ts +3 -1
  53. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  54. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  55. package/src/components/ui/finance/transactions/form.tsx +2 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  57. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  58. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  59. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  60. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  61. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  62. package/src/components/ui/finance/wallets/form.tsx +15 -4
  63. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  64. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  65. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  66. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  67. package/src/components/ui/input-otp.tsx +1 -1
  68. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  69. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  70. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  71. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  72. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  73. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  74. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  75. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  76. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  77. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  78. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  79. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  80. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  81. package/src/components/ui/navigation-menu.tsx +1 -1
  82. package/src/components/ui/pagination.tsx +1 -1
  83. package/src/components/ui/radio-group.tsx +1 -1
  84. package/src/components/ui/select.tsx +5 -1
  85. package/src/components/ui/sheet.tsx +1 -1
  86. package/src/components/ui/sidebar.tsx +1 -1
  87. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  88. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  89. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  90. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  91. package/src/components/ui/storefront/listing-card.tsx +1 -1
  92. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  93. package/src/components/ui/storefront/product-detail.tsx +1 -1
  94. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  95. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  96. package/src/components/ui/storefront/types.ts +4 -0
  97. package/src/components/ui/storefront/utils.ts +6 -0
  98. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  99. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  100. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  101. package/src/components/ui/text-editor/editor.tsx +69 -14
  102. package/src/components/ui/text-editor/extensions.ts +8 -2
  103. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  104. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  105. package/src/components/ui/toast.tsx +1 -1
  106. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  107. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  108. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  109. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
  110. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  111. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  112. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  113. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  114. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  115. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  116. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  117. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  118. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  119. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  120. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  121. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  122. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  127. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  128. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  129. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  131. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
  132. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
  133. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  134. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  135. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  136. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
  137. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
  138. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  139. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  140. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  141. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  142. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  143. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  144. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  145. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  146. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  147. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  148. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  149. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  150. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  151. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  152. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  153. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  154. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
  155. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  156. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  157. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  158. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  159. package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
  160. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  161. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  162. package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
  163. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  164. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  165. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  166. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  167. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  168. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  169. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  170. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  171. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  172. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
  173. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  174. package/src/declarations.d.ts +1 -0
  175. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  176. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  177. package/src/hooks/use-calendar-sync.tsx +247 -243
  178. package/src/hooks/use-calendar.tsx +323 -138
  179. package/src/hooks/use-task-actions.ts +24 -0
  180. package/src/hooks/use-user-workspace-config.ts +75 -0
  181. package/src/hooks/use-workspace-currency.ts +8 -3
  182. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
@@ -1,15 +1,61 @@
1
1
  'use client';
2
2
 
3
- import { AlertTriangle, CalendarClock } from '@tuturuuu/icons';
3
+ import {
4
+ AlertTriangle,
5
+ ArrowDownAZ,
6
+ ArrowUpAZ,
7
+ CalendarClock,
8
+ ChevronLeft,
9
+ ChevronRight,
10
+ ExternalLink,
11
+ FileText,
12
+ Filter,
13
+ Pin,
14
+ PinOff,
15
+ RotateCcw,
16
+ } from '@tuturuuu/icons';
4
17
  import type { Task } from '@tuturuuu/types/primitives/Task';
5
18
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
6
19
  import { Badge } from '@tuturuuu/ui/badge';
20
+ import { Button } from '@tuturuuu/ui/button';
21
+ import { Card } from '@tuturuuu/ui/card';
22
+ import {
23
+ DropdownMenu,
24
+ DropdownMenuCheckboxItem,
25
+ DropdownMenuContent,
26
+ DropdownMenuItem,
27
+ DropdownMenuRadioGroup,
28
+ DropdownMenuRadioItem,
29
+ DropdownMenuSeparator,
30
+ DropdownMenuTrigger,
31
+ } from '@tuturuuu/ui/dropdown-menu';
7
32
  import { cn } from '@tuturuuu/utils/format';
33
+ import { useMemo, useState } from 'react';
8
34
  import { TaskCard } from '../../task';
9
35
  import type { KanbanDeadlineSections } from './kanban-deadline-tasks';
10
36
 
37
+ export type KanbanDeadlineSection = keyof KanbanDeadlineSections;
38
+ export type KanbanDeadlineCollapsedState = Partial<
39
+ Record<KanbanDeadlineSection, boolean>
40
+ >;
41
+
11
42
  export interface KanbanDeadlineLabels {
43
+ collapseSection?: (name: string) => string;
44
+ expandSection?: (name: string) => string;
45
+ filter?: string;
12
46
  overdue: string;
47
+ pinSection?: (name: string) => string;
48
+ reset?: string;
49
+ showDocuments?: string;
50
+ showExternalTasks?: string;
51
+ sort?: string;
52
+ sortCreatedAsc?: string;
53
+ sortCreatedDesc?: string;
54
+ sortDueAsc?: string;
55
+ sortDueDesc?: string;
56
+ sortNameAsc?: string;
57
+ sortSourceAsc?: string;
58
+ unpinSection?: (name: string) => string;
13
59
  upcoming: string;
14
60
  }
15
61
 
@@ -20,10 +66,21 @@ interface KanbanDeadlinePanelsProps {
20
66
  isPersonalWorkspace: boolean;
21
67
  labels: KanbanDeadlineLabels;
22
68
  onClearSelection: () => void;
69
+ onSectionCollapsedChange?: (
70
+ section: KanbanDeadlineSection,
71
+ collapsed: boolean
72
+ ) => void;
73
+ onSectionPinnedChange?: (
74
+ section: KanbanDeadlineSection,
75
+ pinned: boolean
76
+ ) => void;
23
77
  onTaskSelect: (taskId: string, event: React.MouseEvent) => void;
24
78
  onUpdate: () => void;
25
79
  optimisticUpdateInProgress: Set<string>;
26
80
  sections: KanbanDeadlineSections;
81
+ collapsedSections?: KanbanDeadlineCollapsedState;
82
+ pinnedSections?: KanbanDeadlineCollapsedState;
83
+ deadlineNow?: number;
27
84
  selectedTasks: Set<string>;
28
85
  taskLists: TaskList[];
29
86
  isMultiSelectMode: boolean;
@@ -33,11 +90,102 @@ interface KanbanDeadlinePanelsProps {
33
90
  interface DeadlineSectionConfig {
34
91
  icon: typeof AlertTriangle;
35
92
  label: string;
93
+ collapsedClassName: string;
36
94
  panelClassName: string;
37
- section: keyof KanbanDeadlineSections;
95
+ section: KanbanDeadlineSection;
38
96
  titleClassName: string;
39
97
  }
40
98
 
99
+ type DeadlineTaskSortBy =
100
+ | 'created-asc'
101
+ | 'created-desc'
102
+ | 'due-asc'
103
+ | 'due-desc'
104
+ | 'name-asc'
105
+ | 'source-asc';
106
+
107
+ const DEFAULT_DEADLINE_TASK_SORT_BY: DeadlineTaskSortBy = 'due-asc';
108
+ const DOCUMENT_LIST_STATUS = 'documents';
109
+
110
+ function getTaskTime(value: string | null | undefined) {
111
+ if (!value) return null;
112
+ const time = new Date(value).getTime();
113
+ return Number.isFinite(time) ? time : null;
114
+ }
115
+
116
+ function compareNullableTaskTime(
117
+ a: string | null | undefined,
118
+ b: string | null | undefined,
119
+ ascending: boolean
120
+ ) {
121
+ const aTime = getTaskTime(a);
122
+ const bTime = getTaskTime(b);
123
+
124
+ if (aTime === null && bTime === null) return 0;
125
+ if (aTime === null) return 1;
126
+ if (bTime === null) return -1;
127
+
128
+ return ascending ? aTime - bTime : bTime - aTime;
129
+ }
130
+
131
+ function getDeadlineTaskSourceSortText(task: Task) {
132
+ return [
133
+ task.source_workspace_name,
134
+ task.source_board_name,
135
+ task.source_list_name,
136
+ task.name,
137
+ ]
138
+ .filter(Boolean)
139
+ .join(' / ')
140
+ .toLowerCase();
141
+ }
142
+
143
+ function sortDeadlineTasks(tasks: Task[], sortBy: DeadlineTaskSortBy) {
144
+ const sorted = [...tasks];
145
+
146
+ sorted.sort((a, b) => {
147
+ switch (sortBy) {
148
+ case 'created-asc':
149
+ return (
150
+ compareNullableTaskTime(a.created_at, b.created_at, true) ||
151
+ a.name.localeCompare(b.name)
152
+ );
153
+ case 'created-desc':
154
+ return (
155
+ compareNullableTaskTime(a.created_at, b.created_at, false) ||
156
+ a.name.localeCompare(b.name)
157
+ );
158
+ case 'due-desc':
159
+ return (
160
+ compareNullableTaskTime(a.end_date, b.end_date, false) ||
161
+ compareNullableTaskTime(a.created_at, b.created_at, false) ||
162
+ a.name.localeCompare(b.name)
163
+ );
164
+ case 'name-asc':
165
+ return (
166
+ a.name.localeCompare(b.name) ||
167
+ compareNullableTaskTime(a.end_date, b.end_date, true)
168
+ );
169
+ case 'source-asc':
170
+ return (
171
+ getDeadlineTaskSourceSortText(a).localeCompare(
172
+ getDeadlineTaskSourceSortText(b)
173
+ ) ||
174
+ compareNullableTaskTime(a.end_date, b.end_date, true) ||
175
+ a.name.localeCompare(b.name)
176
+ );
177
+ default:
178
+ return (
179
+ compareNullableTaskTime(a.end_date, b.end_date, true) ||
180
+ compareNullableTaskTime(a.created_at, b.created_at, false) ||
181
+ a.name.localeCompare(b.name)
182
+ );
183
+ }
184
+ });
185
+
186
+ return sorted;
187
+ }
188
+
41
189
  function getFallbackTaskList(lists: TaskList[]) {
42
190
  return (
43
191
  lists.find((list) => list.status === 'active') ??
@@ -46,6 +194,21 @@ function getFallbackTaskList(lists: TaskList[]) {
46
194
  );
47
195
  }
48
196
 
197
+ function isDocumentDeadlineTask(task: Task, taskList?: TaskList) {
198
+ return (
199
+ task.source_list_status === DOCUMENT_LIST_STATUS ||
200
+ taskList?.status === DOCUMENT_LIST_STATUS
201
+ );
202
+ }
203
+
204
+ function isExternalDeadlineTask(task: Task, taskList?: TaskList) {
205
+ return (
206
+ task.is_personal_external === true ||
207
+ taskList?.is_external_staging === true ||
208
+ Boolean(task.source_workspace_id)
209
+ );
210
+ }
211
+
49
212
  function getTaskListForDeadlineTask(task: Task, lists: TaskList[]) {
50
213
  return (
51
214
  lists.find((list) => String(list.id) === String(task.list_id)) ??
@@ -57,13 +220,19 @@ function DeadlineSection({
57
220
  availableLists,
58
221
  boardId,
59
222
  bulkUpdateCustomDueDate,
223
+ collapsed,
60
224
  config,
225
+ deadlineNow,
226
+ labels,
61
227
  isMultiSelectMode,
62
228
  isPersonalWorkspace,
63
229
  onClearSelection,
230
+ onCollapsedChange,
231
+ onPinnedChange,
64
232
  onTaskSelect,
65
233
  onUpdate,
66
234
  optimisticUpdateInProgress,
235
+ pinned,
67
236
  selectedTasks,
68
237
  taskLists,
69
238
  tasks,
@@ -72,31 +241,112 @@ function DeadlineSection({
72
241
  availableLists: TaskList[];
73
242
  boardId: string;
74
243
  bulkUpdateCustomDueDate: (date: Date | null) => Promise<void>;
244
+ collapsed: boolean;
75
245
  config: DeadlineSectionConfig;
246
+ deadlineNow?: number;
247
+ labels: KanbanDeadlineLabels;
76
248
  isMultiSelectMode: boolean;
77
249
  isPersonalWorkspace: boolean;
78
250
  onClearSelection: () => void;
251
+ onCollapsedChange?: (
252
+ section: KanbanDeadlineSection,
253
+ collapsed: boolean
254
+ ) => void;
255
+ onPinnedChange?: (section: KanbanDeadlineSection, pinned: boolean) => void;
79
256
  onTaskSelect: (taskId: string, event: React.MouseEvent) => void;
80
257
  onUpdate: () => void;
81
258
  optimisticUpdateInProgress: Set<string>;
259
+ pinned?: boolean;
82
260
  selectedTasks: Set<string>;
83
261
  taskLists: TaskList[];
84
262
  tasks: Task[];
85
263
  workspaceId: string;
86
264
  }) {
265
+ const Icon = config.icon;
266
+ const collapseLabel =
267
+ labels.collapseSection?.(config.label) ?? `Collapse ${config.label}`;
268
+ const expandLabel =
269
+ labels.expandSection?.(config.label) ?? `Expand ${config.label}`;
270
+ const pinLabel = pinned
271
+ ? (labels.unpinSection?.(config.label) ?? `Unpin ${config.label}`)
272
+ : (labels.pinSection?.(config.label) ?? `Pin ${config.label}`);
273
+ const [includeDocuments, setIncludeDocuments] = useState(true);
274
+ const [includeExternal, setIncludeExternal] = useState(true);
275
+ const [sortBy, setSortBy] = useState<DeadlineTaskSortBy>(
276
+ DEFAULT_DEADLINE_TASK_SORT_BY
277
+ );
278
+ const taskListById = useMemo(
279
+ () => new Map(taskLists.map((list) => [String(list.id), list] as const)),
280
+ [taskLists]
281
+ );
282
+ const visibleTasks = useMemo(() => {
283
+ const filteredTasks = tasks.filter((task) => {
284
+ const taskList = taskListById.get(String(task.list_id));
285
+
286
+ if (!includeDocuments && isDocumentDeadlineTask(task, taskList)) {
287
+ return false;
288
+ }
289
+
290
+ if (!includeExternal && isExternalDeadlineTask(task, taskList)) {
291
+ return false;
292
+ }
293
+
294
+ return true;
295
+ });
296
+
297
+ return sortDeadlineTasks(filteredTasks, sortBy);
298
+ }, [includeDocuments, includeExternal, sortBy, taskListById, tasks]);
299
+ const filterCount = (includeDocuments ? 0 : 1) + (includeExternal ? 0 : 1);
300
+
87
301
  if (tasks.length === 0) return null;
88
302
 
89
- const Icon = config.icon;
303
+ if (collapsed) {
304
+ return (
305
+ <Card
306
+ className={cn(
307
+ 'group flex h-full w-14 shrink-0 snap-start flex-col items-center overflow-hidden rounded-xl border border-dashed transition-all duration-200 hover:shadow-md',
308
+ config.collapsedClassName
309
+ )}
310
+ data-testid={`kanban-deadline-section-${config.section}-collapsed`}
311
+ >
312
+ <button
313
+ type="button"
314
+ className={cn(
315
+ 'flex h-full w-full flex-col items-center gap-3 rounded-xl px-1 py-3 transition-colors focus-visible:outline-none focus-visible:ring-2',
316
+ config.titleClassName
317
+ )}
318
+ title={expandLabel}
319
+ aria-label={expandLabel}
320
+ onClick={() => onCollapsedChange?.(config.section, false)}
321
+ >
322
+ <ChevronRight className="h-4 w-4 shrink-0" />
323
+ <Badge
324
+ variant="secondary"
325
+ className="h-5 min-w-5 justify-center px-1 text-[10px]"
326
+ >
327
+ {visibleTasks.length}
328
+ </Badge>
329
+ <span
330
+ className="max-h-48 truncate font-medium text-[11px]"
331
+ style={{ writingMode: 'vertical-rl' }}
332
+ >
333
+ {config.label}
334
+ </span>
335
+ </button>
336
+ </Card>
337
+ );
338
+ }
90
339
 
91
340
  return (
92
- <div
341
+ <Card
93
342
  className={cn(
94
- 'flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border shadow-xs',
343
+ 'flex h-full w-[var(--kanban-column-width)] shrink-0 snap-start flex-col overflow-hidden rounded-xl border border-dashed shadow-xs',
95
344
  config.panelClassName
96
345
  )}
346
+ data-testid={`kanban-deadline-section-${config.section}`}
97
347
  >
98
- <div className="flex items-center justify-between gap-3 border-border/70 border-b px-3 py-2">
99
- <div className="flex min-w-0 items-center gap-2">
348
+ <div className="flex items-center justify-between gap-3 border-border/70 border-b p-3">
349
+ <div className="flex min-w-0 flex-1 items-center gap-2">
100
350
  <span
101
351
  className={cn(
102
352
  'inline-flex size-6 shrink-0 items-center justify-center rounded-md border bg-background/70',
@@ -113,14 +363,174 @@ function DeadlineSection({
113
363
  >
114
364
  {config.label}
115
365
  </h3>
366
+ <Badge
367
+ className="h-5 px-1.5 text-[10px]"
368
+ data-testid={`kanban-deadline-section-${config.section}-count`}
369
+ variant="outline"
370
+ >
371
+ {visibleTasks.length}
372
+ </Badge>
373
+ </div>
374
+ <div className="flex items-center gap-1">
375
+ <DropdownMenu>
376
+ <DropdownMenuTrigger asChild>
377
+ <Button
378
+ type="button"
379
+ variant="ghost"
380
+ size="xs"
381
+ className={cn(
382
+ 'relative h-7 w-7 p-0 hover:bg-muted/40',
383
+ config.titleClassName,
384
+ filterCount > 0 && 'bg-muted/40'
385
+ )}
386
+ title={labels.filter ?? 'Filters'}
387
+ aria-label={labels.filter ?? 'Filters'}
388
+ >
389
+ <Filter className="h-3.5 w-3.5" />
390
+ {filterCount > 0 && (
391
+ <span className="absolute -top-0.5 -right-0.5 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-current px-0.5 font-medium text-[9px] text-background">
392
+ {filterCount}
393
+ </span>
394
+ )}
395
+ </Button>
396
+ </DropdownMenuTrigger>
397
+ <DropdownMenuContent align="end" className="w-64">
398
+ <DropdownMenuCheckboxItem
399
+ checked={includeDocuments}
400
+ onCheckedChange={(checked) =>
401
+ setIncludeDocuments(checked === true)
402
+ }
403
+ onSelect={(event) => event.preventDefault()}
404
+ >
405
+ <FileText className="mr-2 h-3.5 w-3.5 text-dynamic-blue" />
406
+ {labels.showDocuments ?? 'Show document-list tasks'}
407
+ </DropdownMenuCheckboxItem>
408
+ <DropdownMenuCheckboxItem
409
+ checked={includeExternal}
410
+ onCheckedChange={(checked) =>
411
+ setIncludeExternal(checked === true)
412
+ }
413
+ onSelect={(event) => event.preventDefault()}
414
+ >
415
+ <ExternalLink className="mr-2 h-3.5 w-3.5 text-dynamic-cyan" />
416
+ {labels.showExternalTasks ?? 'External tasks'}
417
+ </DropdownMenuCheckboxItem>
418
+ {filterCount > 0 && (
419
+ <>
420
+ <DropdownMenuSeparator />
421
+ <DropdownMenuItem
422
+ onClick={() => {
423
+ setIncludeDocuments(true);
424
+ setIncludeExternal(true);
425
+ }}
426
+ >
427
+ <RotateCcw className="mr-2 h-3.5 w-3.5" />
428
+ {labels.reset ?? 'Reset'}
429
+ </DropdownMenuItem>
430
+ </>
431
+ )}
432
+ </DropdownMenuContent>
433
+ </DropdownMenu>
434
+ <DropdownMenu>
435
+ <DropdownMenuTrigger asChild>
436
+ <Button
437
+ type="button"
438
+ variant="ghost"
439
+ size="xs"
440
+ className={cn(
441
+ 'h-7 w-7 p-0 hover:bg-muted/40',
442
+ config.titleClassName,
443
+ sortBy !== DEFAULT_DEADLINE_TASK_SORT_BY && 'bg-muted/40'
444
+ )}
445
+ title={labels.sort ?? 'Sort'}
446
+ aria-label={labels.sort ?? 'Sort'}
447
+ >
448
+ {sortBy === 'created-asc' ||
449
+ sortBy === 'due-asc' ||
450
+ sortBy === 'name-asc' ||
451
+ sortBy === 'source-asc' ? (
452
+ <ArrowUpAZ className="h-3.5 w-3.5" />
453
+ ) : (
454
+ <ArrowDownAZ className="h-3.5 w-3.5" />
455
+ )}
456
+ </Button>
457
+ </DropdownMenuTrigger>
458
+ <DropdownMenuContent align="end" className="w-56">
459
+ <DropdownMenuRadioGroup
460
+ value={sortBy}
461
+ onValueChange={(value) =>
462
+ setSortBy(value as DeadlineTaskSortBy)
463
+ }
464
+ >
465
+ <DropdownMenuRadioItem value="due-asc">
466
+ <CalendarClock className="mr-2 h-3.5 w-3.5" />
467
+ {labels.sortDueAsc ?? 'Soonest first'}
468
+ </DropdownMenuRadioItem>
469
+ <DropdownMenuRadioItem value="due-desc">
470
+ <CalendarClock className="mr-2 h-3.5 w-3.5" />
471
+ {labels.sortDueDesc ?? 'Latest first'}
472
+ </DropdownMenuRadioItem>
473
+ <DropdownMenuRadioItem value="created-desc">
474
+ <ArrowDownAZ className="mr-2 h-3.5 w-3.5" />
475
+ {labels.sortCreatedDesc ?? 'Newest first'}
476
+ </DropdownMenuRadioItem>
477
+ <DropdownMenuRadioItem value="created-asc">
478
+ <ArrowUpAZ className="mr-2 h-3.5 w-3.5" />
479
+ {labels.sortCreatedAsc ?? 'Oldest first'}
480
+ </DropdownMenuRadioItem>
481
+ <DropdownMenuRadioItem value="name-asc">
482
+ <ArrowUpAZ className="mr-2 h-3.5 w-3.5" />
483
+ {labels.sortNameAsc ?? 'Task name'}
484
+ </DropdownMenuRadioItem>
485
+ <DropdownMenuRadioItem value="source-asc">
486
+ <ArrowUpAZ className="mr-2 h-3.5 w-3.5" />
487
+ {labels.sortSourceAsc ?? 'Source board'}
488
+ </DropdownMenuRadioItem>
489
+ </DropdownMenuRadioGroup>
490
+ </DropdownMenuContent>
491
+ </DropdownMenu>
492
+ {onPinnedChange ? (
493
+ <Button
494
+ type="button"
495
+ variant="ghost"
496
+ size="xs"
497
+ className={cn(
498
+ 'h-7 w-7 p-0 hover:bg-muted/40',
499
+ config.titleClassName,
500
+ pinned && 'bg-muted/40'
501
+ )}
502
+ title={pinLabel}
503
+ aria-label={pinLabel}
504
+ onClick={() => onPinnedChange(config.section, !pinned)}
505
+ >
506
+ {pinned ? (
507
+ <PinOff className="h-3.5 w-3.5" />
508
+ ) : (
509
+ <Pin className="h-3.5 w-3.5" />
510
+ )}
511
+ </Button>
512
+ ) : null}
513
+ {!pinned && (
514
+ <Button
515
+ type="button"
516
+ variant="ghost"
517
+ size="xs"
518
+ className={cn(
519
+ 'h-7 w-7 p-0 hover:bg-muted/40',
520
+ config.titleClassName
521
+ )}
522
+ title={collapseLabel}
523
+ aria-label={collapseLabel}
524
+ onClick={() => onCollapsedChange?.(config.section, true)}
525
+ >
526
+ <ChevronLeft className="h-3.5 w-3.5" />
527
+ </Button>
528
+ )}
116
529
  </div>
117
- <Badge className="h-5 px-1.5 text-[10px]" variant="outline">
118
- {tasks.length}
119
- </Badge>
120
530
  </div>
121
531
 
122
532
  <div className="scrollbar-thin min-h-0 flex-1 space-y-2 overflow-y-auto p-2">
123
- {tasks.map((task) => {
533
+ {visibleTasks.map((task) => {
124
534
  const taskList = getTaskListForDeadlineTask(task, taskLists);
125
535
 
126
536
  return (
@@ -142,6 +552,8 @@ function DeadlineSection({
142
552
  onUpdate={onUpdate}
143
553
  optimisticUpdateInProgress={optimisticUpdateInProgress}
144
554
  selectedTasks={selectedTasks}
555
+ deadlineContext={config.section}
556
+ deadlineNow={deadlineNow}
145
557
  sortableId={`deadline-${config.section}-${task.id}`}
146
558
  task={task}
147
559
  taskList={taskList}
@@ -151,7 +563,7 @@ function DeadlineSection({
151
563
  );
152
564
  })}
153
565
  </div>
154
- </div>
566
+ </Card>
155
567
  );
156
568
  }
157
569
 
@@ -163,10 +575,15 @@ export function KanbanDeadlinePanels({
163
575
  isPersonalWorkspace,
164
576
  labels,
165
577
  onClearSelection,
578
+ onSectionCollapsedChange,
579
+ onSectionPinnedChange,
166
580
  onTaskSelect,
167
581
  onUpdate,
168
582
  optimisticUpdateInProgress,
169
583
  sections,
584
+ collapsedSections,
585
+ pinnedSections,
586
+ deadlineNow,
170
587
  selectedTasks,
171
588
  taskLists,
172
589
  workspaceId,
@@ -179,43 +596,50 @@ export function KanbanDeadlinePanels({
179
596
  {
180
597
  icon: AlertTriangle,
181
598
  label: labels.overdue,
599
+ collapsedClassName: 'border-dynamic-red/35 bg-dynamic-red/5',
182
600
  panelClassName: 'border-dynamic-red/25 bg-dynamic-red/5',
183
601
  section: 'overdue',
184
- titleClassName: 'border-dynamic-red/25 text-dynamic-red',
602
+ titleClassName:
603
+ 'border-dynamic-red/25 text-dynamic-red hover:bg-dynamic-red/10 focus-visible:ring-dynamic-red/40',
185
604
  },
186
605
  {
187
606
  icon: CalendarClock,
188
607
  label: labels.upcoming,
608
+ collapsedClassName: 'border-dynamic-blue/35 bg-dynamic-blue/5',
189
609
  panelClassName: 'border-dynamic-blue/25 bg-dynamic-blue/5',
190
610
  section: 'upcoming',
191
- titleClassName: 'border-dynamic-blue/25 text-dynamic-blue',
611
+ titleClassName:
612
+ 'border-dynamic-blue/25 text-dynamic-blue hover:bg-dynamic-blue/10 focus-visible:ring-dynamic-blue/40',
192
613
  },
193
614
  ];
194
615
 
195
616
  return (
196
- <aside
197
- className="flex h-full w-[18rem] shrink-0 snap-start flex-col gap-3 md:w-80"
198
- data-testid="kanban-deadline-panels"
199
- >
617
+ <div className="contents" data-testid="kanban-deadline-panels">
200
618
  {configs.map((config) => (
201
619
  <DeadlineSection
202
620
  key={config.section}
203
621
  availableLists={availableLists}
204
622
  boardId={boardId}
205
623
  bulkUpdateCustomDueDate={bulkUpdateCustomDueDate}
624
+ collapsed={collapsedSections?.[config.section] === true}
206
625
  config={config}
626
+ deadlineNow={deadlineNow}
627
+ labels={labels}
207
628
  isMultiSelectMode={isMultiSelectMode}
208
629
  isPersonalWorkspace={isPersonalWorkspace}
209
630
  onClearSelection={onClearSelection}
631
+ onCollapsedChange={onSectionCollapsedChange}
632
+ onPinnedChange={onSectionPinnedChange}
210
633
  onTaskSelect={onTaskSelect}
211
634
  onUpdate={onUpdate}
212
635
  optimisticUpdateInProgress={optimisticUpdateInProgress}
636
+ pinned={pinnedSections?.[config.section] === true}
213
637
  selectedTasks={selectedTasks}
214
638
  taskLists={taskLists}
215
639
  tasks={sections[config.section]}
216
640
  workspaceId={workspaceId}
217
641
  />
218
642
  ))}
219
- </aside>
643
+ </div>
220
644
  );
221
645
  }