@tuturuuu/ui 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/biome.json +1 -1
  3. package/package.json +74 -71
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
  29. package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
  30. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  31. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +46 -1
  32. package/src/components/ui/custom/combobox.test.tsx +195 -0
  33. package/src/components/ui/custom/combobox.tsx +273 -156
  34. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  35. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  36. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  37. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  38. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  39. package/src/components/ui/custom/nav-link.test.tsx +165 -0
  40. package/src/components/ui/custom/nav-link.tsx +69 -11
  41. package/src/components/ui/custom/navigation.tsx +1 -0
  42. package/src/components/ui/custom/settings/task-settings.tsx +104 -0
  43. package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
  44. package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
  45. package/src/components/ui/custom/settings-dialog-search.ts +75 -0
  46. package/src/components/ui/custom/settings-dialog-shell.tsx +65 -28
  47. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  48. package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
  49. package/src/components/ui/custom/workspace-select.tsx +25 -19
  50. package/src/components/ui/dialog.test.tsx +52 -0
  51. package/src/components/ui/dialog.tsx +6 -2
  52. package/src/components/ui/dropdown-menu.tsx +5 -1
  53. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  54. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  55. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  56. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  57. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  58. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  59. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  60. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  61. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  62. package/src/components/ui/finance/invoices/utils.ts +3 -1
  63. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  64. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  65. package/src/components/ui/finance/transactions/form.tsx +2 -0
  66. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  67. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  68. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  69. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  70. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  71. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  72. package/src/components/ui/finance/wallets/form.tsx +15 -4
  73. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  75. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  76. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  77. package/src/components/ui/input-otp.tsx +1 -1
  78. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  79. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  80. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  81. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  82. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  83. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  84. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  85. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  86. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  87. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  88. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  89. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  90. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  91. package/src/components/ui/navigation-menu.tsx +1 -1
  92. package/src/components/ui/pagination.tsx +1 -1
  93. package/src/components/ui/radio-group.tsx +1 -1
  94. package/src/components/ui/select.tsx +5 -1
  95. package/src/components/ui/sheet.tsx +1 -1
  96. package/src/components/ui/sidebar.tsx +1 -1
  97. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  98. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  99. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  100. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  101. package/src/components/ui/storefront/listing-card.tsx +1 -1
  102. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  103. package/src/components/ui/storefront/product-detail.tsx +1 -1
  104. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  105. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  106. package/src/components/ui/storefront/types.ts +4 -0
  107. package/src/components/ui/storefront/utils.ts +6 -0
  108. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  109. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  110. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  111. package/src/components/ui/text-editor/editor.tsx +69 -14
  112. package/src/components/ui/text-editor/extensions.ts +8 -2
  113. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  114. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  115. package/src/components/ui/toast.tsx +1 -1
  116. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +286 -0
  117. package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
  118. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  119. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +15 -226
  120. package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
  121. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +121 -39
  122. package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
  127. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  128. package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
  129. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  131. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
  132. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  133. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  134. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  135. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  136. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  139. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  141. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  142. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  143. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +642 -5
  148. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +224 -15
  149. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +535 -53
  150. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +101 -33
  151. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +235 -113
  152. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +50 -5
  153. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +12 -2
  154. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +10 -1
  155. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  156. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
  157. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
  158. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +271 -36
  159. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  160. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  161. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +22 -0
  162. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
  163. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
  164. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
  165. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
  166. package/src/components/ui/tu-do/boards/form.tsx +1 -1
  167. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  168. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  169. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  170. package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
  171. package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
  172. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
  173. package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
  174. package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
  175. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  176. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  177. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  178. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  179. package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
  180. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
  181. package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
  182. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
  183. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
  184. package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
  185. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +141 -1
  186. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +377 -36
  187. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +374 -0
  188. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +419 -5
  189. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +38 -0
  190. package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
  191. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  192. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
  193. package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
  194. package/src/components/ui/tu-do/shared/board-client.tsx +15 -10
  195. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  196. package/src/components/ui/tu-do/shared/board-header.tsx +471 -975
  197. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  198. package/src/components/ui/tu-do/shared/board-switcher.tsx +244 -220
  199. package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
  200. package/src/components/ui/tu-do/shared/board-views.tsx +577 -85
  201. package/src/components/ui/tu-do/shared/list-view.tsx +246 -2
  202. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  203. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  204. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  205. package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
  206. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
  210. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  211. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
  212. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
  213. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
  214. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
  215. package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
  216. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
  217. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
  218. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
  219. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
  220. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
  221. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  222. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  223. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  224. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
  225. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
  226. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
  227. package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
  228. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  229. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +11 -1
  230. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  231. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
  232. package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
  233. package/src/declarations.d.ts +1 -0
  234. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  235. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  236. package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
  237. package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
  238. package/src/hooks/use-calendar-sync.tsx +247 -243
  239. package/src/hooks/use-calendar.tsx +323 -138
  240. package/src/hooks/use-task-actions.ts +24 -0
  241. package/src/hooks/use-user-workspace-config.ts +75 -0
  242. package/src/hooks/use-workspace-currency.ts +8 -3
  243. package/src/hooks/useBoardPresence.ts +364 -0
  244. package/src/hooks/useBoardRealtimeEventHandler.ts +45 -90
  245. package/src/lib/workspace-actions.ts +2 -6
@@ -61,6 +61,7 @@ import { useLocale, useTranslations } from 'next-intl';
61
61
  import { useTheme } from 'next-themes';
62
62
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
63
63
  import { useBulkOperations } from '../boards/boardId/kanban/bulk/bulk-operations';
64
+ import type { TaskCardAssigneeMemberSource } from '../boards/boardId/task-card/task-card';
64
65
  import {
65
66
  getTaskCardHydratingOpenOptions,
66
67
  isExternalTaskSnapshot,
@@ -85,9 +86,12 @@ interface Props {
85
86
  tasks: Task[];
86
87
  lists: TaskList[];
87
88
  isPersonalWorkspace?: boolean;
89
+ canUseBoardAssignees?: boolean;
90
+ assigneeMemberSource?: TaskCardAssigneeMemberSource;
88
91
  preserveTaskOrder?: boolean;
89
92
  searchQuery?: string;
90
93
  weekStartsOn?: 0 | 1 | 6;
94
+ readOnly?: boolean;
91
95
  }
92
96
 
93
97
  interface ColumnVisibility {
@@ -105,12 +109,241 @@ type TaskMenuState = {
105
109
  point?: { x: number; y: number } | null;
106
110
  };
107
111
 
108
- export function ListView({
112
+ export function ListView(props: Props) {
113
+ if (props.readOnly) {
114
+ return <ReadOnlyListView {...props} />;
115
+ }
116
+
117
+ return <InteractiveListView {...props} />;
118
+ }
119
+
120
+ function ReadOnlyListView({
121
+ tasks,
122
+ lists,
123
+ isPersonalWorkspace = false,
124
+ canUseBoardAssignees,
125
+ preserveTaskOrder = false,
126
+ searchQuery,
127
+ }: Props) {
128
+ const t = useTranslations();
129
+ const tc = useTranslations('common');
130
+ const locale = useLocale();
131
+ const dateLocale = locale === 'vi' ? vi : enUS;
132
+ const [sortField, setSortField] = useState<ListViewSortField>('created_at');
133
+ const [sortOrder, setSortOrder] = useState<ListViewSortOrder>('desc');
134
+ const showAssignees = canUseBoardAssignees ?? !isPersonalWorkspace;
135
+ const listsById = useMemo(
136
+ () => new Map(lists.map((list) => [list.id, list])),
137
+ [lists]
138
+ );
139
+ const sortedTasks = useMemo(
140
+ () =>
141
+ sortListViewTasks(tasks, {
142
+ preserveTaskOrder,
143
+ searchQuery,
144
+ sortField,
145
+ sortOrder,
146
+ }),
147
+ [preserveTaskOrder, searchQuery, sortField, sortOrder, tasks]
148
+ );
149
+
150
+ function handleSort(field: ListViewSortField) {
151
+ if (field === sortField) {
152
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
153
+ } else {
154
+ setSortField(field);
155
+ setSortOrder('asc');
156
+ }
157
+ }
158
+
159
+ function getSortIcon(field: ListViewSortField) {
160
+ if (sortField !== field) {
161
+ return <ArrowDownUp className="ml-2 h-3 w-3 text-muted-foreground" />;
162
+ }
163
+ return sortOrder === 'asc' ? (
164
+ <ArrowUp className="ml-2 h-3 w-3 text-foreground" />
165
+ ) : (
166
+ <ArrowDown className="ml-2 h-3 w-3 text-foreground" />
167
+ );
168
+ }
169
+
170
+ function formatDate(date: string) {
171
+ return format(new Date(date), 'MMM dd', { locale: dateLocale });
172
+ }
173
+
174
+ return (
175
+ <div className="flex h-full flex-col">
176
+ {sortedTasks.length === 0 ? (
177
+ <div className="flex flex-1 items-center justify-center">
178
+ <p className="text-muted-foreground text-sm">{tc('no_tasks')}</p>
179
+ </div>
180
+ ) : (
181
+ <div className="relative flex-1 overflow-auto">
182
+ <Table>
183
+ <TableHeader className="sticky top-0 z-10 border-b bg-background">
184
+ <TableRow className="hover:bg-transparent">
185
+ <TableHead className="h-9 min-w-62.5 px-3">
186
+ <Button
187
+ variant="ghost"
188
+ className={cn(
189
+ '-ml-2 h-6 justify-start gap-1 px-2 font-medium text-[10px] uppercase tracking-wider transition-colors hover:bg-muted/50',
190
+ sortField === 'name'
191
+ ? 'text-foreground'
192
+ : 'text-muted-foreground'
193
+ )}
194
+ onClick={() => handleSort('name')}
195
+ >
196
+ {tc('task_header')}
197
+ {getSortIcon('name')}
198
+ </Button>
199
+ </TableHead>
200
+ <TableHead className="h-9 w-32 px-2">
201
+ <span className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
202
+ {tc('status')}
203
+ </span>
204
+ </TableHead>
205
+ <TableHead className="h-9 w-24 px-2">
206
+ <Button
207
+ variant="ghost"
208
+ className={cn(
209
+ '-ml-2 h-6 justify-start gap-1 px-2 font-medium text-[10px] uppercase tracking-wider transition-colors hover:bg-muted/50',
210
+ sortField === 'priority'
211
+ ? 'text-foreground'
212
+ : 'text-muted-foreground'
213
+ )}
214
+ onClick={() => handleSort('priority')}
215
+ >
216
+ {tc('priority')}
217
+ {getSortIcon('priority')}
218
+ </Button>
219
+ </TableHead>
220
+ <TableHead className="h-9 w-28 px-2">
221
+ <Button
222
+ variant="ghost"
223
+ className={cn(
224
+ '-ml-2 h-6 justify-start gap-1 px-2 font-medium text-[10px] uppercase tracking-wider transition-colors hover:bg-muted/50',
225
+ sortField === 'end_date'
226
+ ? 'text-foreground'
227
+ : 'text-muted-foreground'
228
+ )}
229
+ onClick={() => handleSort('end_date')}
230
+ >
231
+ {tc('due')}
232
+ {getSortIcon('end_date')}
233
+ </Button>
234
+ </TableHead>
235
+ {showAssignees && (
236
+ <TableHead className="h-9 w-32 px-2">
237
+ <span className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
238
+ {tc('assignee')}
239
+ </span>
240
+ </TableHead>
241
+ )}
242
+ </TableRow>
243
+ </TableHeader>
244
+ <TableBody>
245
+ {sortedTasks.map((task) => {
246
+ const list = listsById.get(task.list_id);
247
+ return (
248
+ <TableRow key={task.id} className="h-12 border-b">
249
+ <TableCell className="px-3 py-2">
250
+ <div className="min-w-0 space-y-1">
251
+ <div
252
+ className={cn(
253
+ 'truncate font-medium text-sm',
254
+ (task.completed_at || task.closed_at) &&
255
+ 'text-muted-foreground line-through'
256
+ )}
257
+ >
258
+ {task.name}
259
+ </div>
260
+ {(task.labels?.length || task.projects?.length) && (
261
+ <div className="flex flex-wrap items-center gap-1">
262
+ {task.labels?.slice(0, 3).map((label) => (
263
+ <Badge
264
+ key={label.id}
265
+ variant="outline"
266
+ className="h-4 gap-1 px-1.5 font-normal text-[10px]"
267
+ >
268
+ <span
269
+ aria-hidden="true"
270
+ className="h-2 w-2 rounded-full"
271
+ style={{ backgroundColor: label.color }}
272
+ />
273
+ {label.name}
274
+ </Badge>
275
+ ))}
276
+ {task.projects?.slice(0, 2).map((project) => (
277
+ <Badge
278
+ key={project.id}
279
+ variant="secondary"
280
+ className="h-4 px-1.5 font-normal text-[10px]"
281
+ >
282
+ {project.name}
283
+ </Badge>
284
+ ))}
285
+ </div>
286
+ )}
287
+ </div>
288
+ </TableCell>
289
+ <TableCell className="px-2 py-2">
290
+ <Badge variant="outline" className="font-normal">
291
+ {list?.name ?? tc('untitled')}
292
+ </Badge>
293
+ </TableCell>
294
+ <TableCell className="px-2 py-2">
295
+ {task.priority && (
296
+ <Badge variant="secondary" className="font-normal">
297
+ {t(`tasks.priority_${task.priority}`)}
298
+ </Badge>
299
+ )}
300
+ </TableCell>
301
+ <TableCell className="px-2 py-2">
302
+ {task.end_date && (
303
+ <span className="text-muted-foreground text-xs">
304
+ {formatDate(task.end_date)}
305
+ </span>
306
+ )}
307
+ </TableCell>
308
+ {showAssignees && (
309
+ <TableCell className="px-2 py-2">
310
+ {task.assignees && task.assignees.length > 0 && (
311
+ <div className="flex flex-wrap gap-1">
312
+ {task.assignees.slice(0, 2).map((assignee) => (
313
+ <Badge
314
+ key={assignee.id}
315
+ variant="secondary"
316
+ className="font-normal"
317
+ >
318
+ {assignee.display_name ||
319
+ assignee.email ||
320
+ assignee.handle ||
321
+ tc('assignee')}
322
+ </Badge>
323
+ ))}
324
+ </div>
325
+ )}
326
+ </TableCell>
327
+ )}
328
+ </TableRow>
329
+ );
330
+ })}
331
+ </TableBody>
332
+ </Table>
333
+ </div>
334
+ )}
335
+ </div>
336
+ );
337
+ }
338
+
339
+ function InteractiveListView({
109
340
  workspaceId,
110
341
  boardId,
111
342
  tasks,
112
343
  lists,
113
344
  isPersonalWorkspace = false,
345
+ canUseBoardAssignees,
346
+ assigneeMemberSource,
114
347
  preserveTaskOrder = false,
115
348
  searchQuery,
116
349
  weekStartsOn = 0,
@@ -134,6 +367,9 @@ export function ListView({
134
367
  const previousWorkspaceIdRef = useRef(workspaceId);
135
368
  const previousBoardIdRef = useRef(boardId);
136
369
  const { openTask, openTaskById } = useTaskDialog();
370
+ const showAssignees = canUseBoardAssignees ?? !isPersonalWorkspace;
371
+ const effectiveAssigneeMemberSource =
372
+ assigneeMemberSource ?? (isPersonalWorkspace ? 'board' : 'workspace');
137
373
 
138
374
  // Infinite scroll
139
375
  const [displayCount, setDisplayCount] = useState(50);
@@ -146,7 +382,7 @@ export function ListView({
146
382
  priority: true,
147
383
  start_date: false,
148
384
  end_date: true,
149
- assignees: !isPersonalWorkspace,
385
+ assignees: showAssignees,
150
386
  actions: true,
151
387
  });
152
388
 
@@ -290,6 +526,10 @@ export function ListView({
290
526
  availableLists: lists,
291
527
  effectiveWorkspaceId: workspaceId,
292
528
  isPersonalWorkspace,
529
+ canUseBoardAssignees: task.source_workspace_id ? true : showAssignees,
530
+ assigneeMemberSource: task.source_workspace_id
531
+ ? 'workspace'
532
+ : effectiveAssigneeMemberSource,
293
533
  })
294
534
  );
295
535
  return;
@@ -298,6 +538,8 @@ export function ListView({
298
538
  openTask(task, boardId, lists, false, {
299
539
  taskWsId: workspaceId,
300
540
  taskWorkspacePersonal: isPersonalWorkspace,
541
+ canUseBoardAssignees: showAssignees,
542
+ assigneeMemberSource: effectiveAssigneeMemberSource,
301
543
  });
302
544
  }
303
545
 
@@ -746,6 +988,8 @@ export function ListView({
746
988
  workspaceId={workspaceId}
747
989
  lists={lists}
748
990
  isPersonalWorkspace={isPersonalWorkspace}
991
+ canUseBoardAssignees={showAssignees}
992
+ assigneeMemberSource={effectiveAssigneeMemberSource}
749
993
  onUpdate={() => {
750
994
  void queryClient.invalidateQueries({
751
995
  queryKey: ['tasks', boardId],
@@ -73,14 +73,24 @@ interface RecycleBinPanelProps {
73
73
  };
74
74
  }
75
75
 
76
- export function RecycleBinPanel({
77
- open,
78
- onOpenChange,
76
+ interface RecycleBinContentProps
77
+ extends Omit<RecycleBinPanelProps, 'open' | 'onOpenChange'> {
78
+ active?: boolean;
79
+ className?: string;
80
+ onParentOpenChange?: (open: boolean) => void;
81
+ showHeader?: boolean;
82
+ }
83
+
84
+ export function RecycleBinContent({
85
+ active = true,
86
+ className,
87
+ onParentOpenChange,
88
+ showHeader = true,
79
89
  wsId,
80
90
  boardId,
81
91
  lists,
82
92
  translations,
83
- }: RecycleBinPanelProps) {
93
+ }: RecycleBinContentProps) {
84
94
  // Use provided translations or fall back to English defaults
85
95
  const t = useMemo(
86
96
  () => ({
@@ -143,7 +153,7 @@ export function RecycleBinPanel({
143
153
  boardId,
144
154
  wsId,
145
155
  {
146
- enabled: open,
156
+ enabled: active,
147
157
  staleTime: 60000,
148
158
  }
149
159
  );
@@ -193,20 +203,20 @@ export function RecycleBinPanel({
193
203
  (isOpen: boolean) => {
194
204
  setRestoreDialogOpen(isOpen);
195
205
  if (isOpen) {
196
- onOpenChange(false); // Close Sheet when dialog opens
206
+ onParentOpenChange?.(false); // Close Sheet when dialog opens
197
207
  }
198
208
  },
199
- [onOpenChange]
209
+ [onParentOpenChange]
200
210
  );
201
211
 
202
212
  const handleDeleteDialogOpenChange = useCallback(
203
213
  (isOpen: boolean) => {
204
214
  setDeleteDialogOpen(isOpen);
205
215
  if (isOpen) {
206
- onOpenChange(false); // Close Sheet when dialog opens
216
+ onParentOpenChange?.(false); // Close Sheet when dialog opens
207
217
  }
208
218
  },
209
- [onOpenChange]
219
+ [onParentOpenChange]
210
220
  );
211
221
 
212
222
  const handleRestore = useCallback(async () => {
@@ -254,97 +264,96 @@ export function RecycleBinPanel({
254
264
 
255
265
  return (
256
266
  <>
257
- <Sheet open={open} onOpenChange={onOpenChange}>
258
- <SheetContent side="right" className="flex w-full flex-col sm:max-w-md">
259
- <SheetHeader>
260
- <SheetTitle className="flex items-center gap-2">
267
+ <div className={cn('flex min-h-0 flex-1 flex-col', className)}>
268
+ {showHeader && (
269
+ <div className="border-b p-4">
270
+ <h2 className="flex items-center gap-2 font-semibold text-lg">
261
271
  <Trash2 className="h-5 w-5" />
262
272
  {t.recycleBin}
263
- </SheetTitle>
264
- <SheetDescription>{t.recycleBinDescription}</SheetDescription>
265
- </SheetHeader>
266
-
267
- <div className="flex flex-1 flex-col overflow-hidden">
268
- {/* Header with select all */}
269
- {deletedTasks.length > 0 && (
270
- <div className="flex items-center gap-3 border-b p-3">
271
- <Checkbox
272
- checked={allSelected}
273
- onCheckedChange={handleSelectAll}
274
- aria-label={t.selectAllTasks}
273
+ </h2>
274
+ <p className="mt-1 text-muted-foreground text-sm">
275
+ {t.recycleBinDescription}
276
+ </p>
277
+ </div>
278
+ )}
279
+ {/* Header with select all */}
280
+ {deletedTasks.length > 0 && (
281
+ <div className="flex items-center gap-3 border-b p-3">
282
+ <Checkbox
283
+ checked={allSelected}
284
+ onCheckedChange={handleSelectAll}
285
+ aria-label={t.selectAllTasks}
286
+ />
287
+ <span className="text-foreground text-sm">
288
+ {someSelected
289
+ ? t.selectedOfTotal
290
+ .replace('{selected}', String(selectedTasks.size))
291
+ .replace('{total}', String(deletedTasks.length))
292
+ : t.deletedTasksCount.replace(
293
+ '{count}',
294
+ String(deletedTasks.length)
295
+ )}
296
+ </span>
297
+ </div>
298
+ )}
299
+
300
+ {/* Task list */}
301
+ <div className="flex-1 overflow-y-auto p-3">
302
+ {isLoading ? (
303
+ <div className="flex items-center justify-center py-12">
304
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
305
+ </div>
306
+ ) : deletedTasks.length === 0 ? (
307
+ <div className="flex flex-col items-center justify-center py-12 text-center">
308
+ <Trash2 className="mb-3 h-12 w-12 text-muted-foreground/50" />
309
+ <p className="text-muted-foreground">{t.noDeletedTasks}</p>
310
+ <p className="mt-1 text-muted-foreground/70 text-xs">
311
+ {t.deletedTasksWillAppearHere}
312
+ </p>
313
+ </div>
314
+ ) : (
315
+ <div className="space-y-2">
316
+ {deletedTasks.map((task: Task) => (
317
+ <RecycleBinTaskRow
318
+ key={task.id}
319
+ task={task}
320
+ listName={listMap.get(task.list_id)}
321
+ isSelected={selectedTasks.has(task.id)}
322
+ onSelect={(checked) => handleSelectTask(task.id, checked)}
323
+ disabled={isWorking}
324
+ translations={t}
275
325
  />
276
- <span className="text-foreground text-sm">
277
- {someSelected
278
- ? t.selectedOfTotal
279
- .replace('{selected}', String(selectedTasks.size))
280
- .replace('{total}', String(deletedTasks.length))
281
- : t.deletedTasksCount.replace(
282
- '{count}',
283
- String(deletedTasks.length)
284
- )}
285
- </span>
286
- </div>
287
- )}
288
-
289
- {/* Task list */}
290
- <div className="flex-1 overflow-y-auto p-3">
291
- {isLoading ? (
292
- <div className="flex items-center justify-center py-12">
293
- <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
294
- </div>
295
- ) : deletedTasks.length === 0 ? (
296
- <div className="flex flex-col items-center justify-center py-12 text-center">
297
- <Trash2 className="mb-3 h-12 w-12 text-muted-foreground/50" />
298
- <p className="text-muted-foreground">{t.noDeletedTasks}</p>
299
- <p className="mt-1 text-muted-foreground/70 text-xs">
300
- {t.deletedTasksWillAppearHere}
301
- </p>
302
- </div>
303
- ) : (
304
- <div className="space-y-2">
305
- {deletedTasks.map((task: Task) => (
306
- <RecycleBinTaskRow
307
- key={task.id}
308
- task={task}
309
- listName={listMap.get(task.list_id)}
310
- isSelected={selectedTasks.has(task.id)}
311
- onSelect={(checked) => handleSelectTask(task.id, checked)}
312
- disabled={isWorking}
313
- translations={t}
314
- />
315
- ))}
316
- </div>
317
- )}
326
+ ))}
318
327
  </div>
328
+ )}
329
+ </div>
319
330
 
320
- {/* Action bar */}
321
- {someSelected && (
322
- <div className="flex items-center gap-2 border-t px-3 py-4">
323
- <Button
324
- variant="outline"
325
- size="sm"
326
- className="flex-1"
327
- onClick={() => setRestoreDialogOpen(true)}
328
- disabled={isWorking}
329
- >
330
- <RotateCcw className="mr-2 h-4 w-4" />
331
- {t.restore} ({selectedTasks.size})
332
- </Button>
333
- <Button
334
- variant="destructive"
335
- size="sm"
336
- className="flex-1"
337
- onClick={() => setDeleteDialogOpen(true)}
338
- disabled={isWorking}
339
- >
340
- <Trash2 className="mr-2 h-4 w-4" />
341
- {t.delete} ({selectedTasks.size})
342
- </Button>
343
- </div>
344
- )}
331
+ {/* Action bar */}
332
+ {someSelected && (
333
+ <div className="flex items-center gap-2 border-t px-3 py-4">
334
+ <Button
335
+ variant="outline"
336
+ size="sm"
337
+ className="flex-1"
338
+ onClick={() => handleRestoreDialogOpenChange(true)}
339
+ disabled={isWorking}
340
+ >
341
+ <RotateCcw className="mr-2 h-4 w-4" />
342
+ {t.restore} ({selectedTasks.size})
343
+ </Button>
344
+ <Button
345
+ variant="destructive"
346
+ size="sm"
347
+ className="flex-1"
348
+ onClick={() => handleDeleteDialogOpenChange(true)}
349
+ disabled={isWorking}
350
+ >
351
+ <Trash2 className="mr-2 h-4 w-4" />
352
+ {t.delete} ({selectedTasks.size})
353
+ </Button>
345
354
  </div>
346
- </SheetContent>
347
- </Sheet>
355
+ )}
356
+ </div>
348
357
 
349
358
  {/* Restore Confirmation Dialog */}
350
359
  <AlertDialog
@@ -433,6 +442,45 @@ export function RecycleBinPanel({
433
442
  );
434
443
  }
435
444
 
445
+ export function RecycleBinPanel({
446
+ open,
447
+ onOpenChange,
448
+ wsId,
449
+ boardId,
450
+ lists,
451
+ translations,
452
+ }: RecycleBinPanelProps) {
453
+ const t = {
454
+ recycleBin: translations?.recycleBin ?? 'Recycle Bin',
455
+ recycleBinDescription:
456
+ translations?.recycleBinDescription ??
457
+ 'Restore or permanently delete tasks that were previously removed.',
458
+ };
459
+
460
+ return (
461
+ <Sheet open={open} onOpenChange={onOpenChange}>
462
+ <SheetContent side="right" className="flex w-full flex-col sm:max-w-md">
463
+ <SheetHeader>
464
+ <SheetTitle className="flex items-center gap-2">
465
+ <Trash2 className="h-5 w-5" />
466
+ {t.recycleBin}
467
+ </SheetTitle>
468
+ <SheetDescription>{t.recycleBinDescription}</SheetDescription>
469
+ </SheetHeader>
470
+ <RecycleBinContent
471
+ active={open}
472
+ boardId={boardId}
473
+ lists={lists}
474
+ onParentOpenChange={onOpenChange}
475
+ showHeader={false}
476
+ translations={translations}
477
+ wsId={wsId}
478
+ />
479
+ </SheetContent>
480
+ </Sheet>
481
+ );
482
+ }
483
+
436
484
  interface RecycleBinTaskRowProps {
437
485
  task: Task;
438
486
  listName?: string;
@@ -0,0 +1,51 @@
1
+ export type SpecialTaskListPin =
2
+ | 'closed_tasks'
3
+ | 'external_tasks'
4
+ | 'overdue'
5
+ | 'upcoming';
6
+
7
+ export const SPECIAL_TASK_LIST_PIN_VALUES: readonly SpecialTaskListPin[] = [
8
+ 'overdue',
9
+ 'upcoming',
10
+ 'external_tasks',
11
+ 'closed_tasks',
12
+ ];
13
+
14
+ const SPECIAL_TASK_LIST_PIN_SET = new Set<string>(SPECIAL_TASK_LIST_PIN_VALUES);
15
+
16
+ export type SpecialTaskListPinState = Partial<
17
+ Record<SpecialTaskListPin, boolean>
18
+ >;
19
+
20
+ export function parseSpecialTaskListPins(
21
+ raw: string | null | undefined
22
+ ): SpecialTaskListPinState {
23
+ if (!raw) return {};
24
+
25
+ const parseValues = (value: unknown) => {
26
+ if (!Array.isArray(value)) return [];
27
+ return value.filter((item): item is SpecialTaskListPin => {
28
+ return typeof item === 'string' && SPECIAL_TASK_LIST_PIN_SET.has(item);
29
+ });
30
+ };
31
+
32
+ let values: SpecialTaskListPin[] = [];
33
+
34
+ try {
35
+ values = parseValues(JSON.parse(raw));
36
+ } catch {
37
+ values = parseValues(raw.split(',').map((item) => item.trim()));
38
+ }
39
+
40
+ return values.reduce<SpecialTaskListPinState>((acc, pin) => {
41
+ acc[pin] = true;
42
+ return acc;
43
+ }, {});
44
+ }
45
+
46
+ export function serializeSpecialTaskListPins(
47
+ state: SpecialTaskListPinState
48
+ ): string | null {
49
+ const pins = SPECIAL_TASK_LIST_PIN_VALUES.filter((pin) => state[pin]);
50
+ return pins.length > 0 ? JSON.stringify(pins) : null;
51
+ }
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@tuturuuu/utils/format';
4
+ import { KanbanSkeleton } from '../boards/boardId/kanban/rendering/kanban-skeleton';
5
+
6
+ export function TaskBoardLoadingState({
7
+ className,
8
+ root = false,
9
+ }: {
10
+ className?: string;
11
+ root?: boolean;
12
+ }) {
13
+ return (
14
+ <div
15
+ aria-busy="true"
16
+ className={cn(
17
+ 'overflow-hidden bg-transparent',
18
+ root
19
+ ? '-m-4 h-[calc(100dvh+2rem)] min-h-[calc(32rem+2rem)] w-[calc(100%+2rem)] min-w-[calc(100%+2rem)]'
20
+ : 'h-[calc(100dvh-1rem)] min-h-[32rem] w-full',
21
+ className
22
+ )}
23
+ data-testid="task-board-loading-state"
24
+ >
25
+ <KanbanSkeleton root={root} />
26
+ </div>
27
+ );
28
+ }