@tuturuuu/ui 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/biome.json +1 -1
  3. package/package.json +75 -73
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/currency-input.test.tsx +43 -0
  29. package/src/components/ui/currency-input.tsx +1 -1
  30. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  31. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  32. package/src/components/ui/custom/combobox.test.tsx +195 -0
  33. package/src/components/ui/custom/combobox.tsx +273 -156
  34. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  35. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  36. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  37. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  38. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  39. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  40. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  41. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  42. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  43. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  44. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  45. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  46. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  47. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  48. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  49. package/src/components/ui/custom/workspace-select.tsx +8 -3
  50. package/src/components/ui/dialog.test.tsx +52 -0
  51. package/src/components/ui/dialog.tsx +6 -2
  52. package/src/components/ui/dropdown-menu.tsx +5 -1
  53. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  54. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  55. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  56. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  57. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  58. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  59. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  60. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  61. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  62. package/src/components/ui/finance/invoices/utils.ts +3 -1
  63. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  64. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  65. package/src/components/ui/finance/transactions/form.tsx +2 -0
  66. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  67. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  68. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  69. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  70. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  71. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  72. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  73. package/src/components/ui/finance/wallets/form.tsx +15 -4
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  75. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  76. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  77. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  78. package/src/components/ui/input-otp.tsx +1 -1
  79. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  80. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  81. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  82. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  83. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  84. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  85. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  86. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  87. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  88. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  89. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  90. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  91. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  92. package/src/components/ui/money-input.test.tsx +64 -0
  93. package/src/components/ui/money-input.tsx +63 -0
  94. package/src/components/ui/navigation-menu.tsx +1 -1
  95. package/src/components/ui/pagination.tsx +1 -1
  96. package/src/components/ui/radio-group.tsx +1 -1
  97. package/src/components/ui/select.tsx +5 -1
  98. package/src/components/ui/sheet.tsx +1 -1
  99. package/src/components/ui/sidebar.tsx +1 -1
  100. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  101. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  102. package/src/components/ui/storefront/cart-summary.tsx +104 -80
  103. package/src/components/ui/storefront/checkout-overlay.tsx +26 -0
  104. package/src/components/ui/storefront/hero-panel.tsx +2 -8
  105. package/src/components/ui/storefront/image-panel.tsx +6 -0
  106. package/src/components/ui/storefront/index.ts +11 -0
  107. package/src/components/ui/storefront/listing-card.tsx +84 -22
  108. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  109. package/src/components/ui/storefront/product-detail.tsx +289 -0
  110. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  111. package/src/components/ui/storefront/storefront-surface.test.tsx +221 -3
  112. package/src/components/ui/storefront/storefront-surface.tsx +288 -153
  113. package/src/components/ui/storefront/types.ts +27 -1
  114. package/src/components/ui/storefront/utils.ts +117 -27
  115. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  116. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  117. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  118. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  119. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  120. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  121. package/src/components/ui/text-editor/content-migration.ts +41 -18
  122. package/src/components/ui/text-editor/editor.tsx +69 -14
  123. package/src/components/ui/text-editor/extensions.ts +9 -3
  124. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  125. package/src/components/ui/text-editor/image-extension.ts +40 -18
  126. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  127. package/src/components/ui/text-editor/video-extension.ts +11 -2
  128. package/src/components/ui/toast.tsx +1 -1
  129. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  130. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  131. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  132. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  133. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  134. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +113 -46
  135. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  136. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  139. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  141. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +51 -9
  142. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  143. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +127 -38
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  148. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  149. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  150. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  151. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  152. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  153. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  154. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  155. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  156. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  157. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +410 -4
  158. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +106 -14
  159. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  160. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  161. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  162. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +186 -0
  163. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +59 -2
  164. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  165. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  166. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  167. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  168. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  169. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  170. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  171. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  172. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  173. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  174. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  175. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  176. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  177. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  178. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  179. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  180. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  181. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  182. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  183. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  184. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  185. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  186. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +237 -3
  187. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  188. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  189. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  190. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  191. package/src/components/ui/tu-do/shared/board-header.tsx +465 -937
  192. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  193. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  194. package/src/components/ui/tu-do/shared/board-views.tsx +596 -82
  195. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  196. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  197. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  198. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  199. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  200. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  201. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  202. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  203. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  204. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  205. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  206. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  210. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +44 -15
  211. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  212. package/src/declarations.d.ts +1 -0
  213. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  214. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  215. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  216. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  217. package/src/hooks/use-calendar-sync.tsx +247 -243
  218. package/src/hooks/use-calendar.tsx +323 -138
  219. package/src/hooks/use-task-actions.ts +24 -0
  220. package/src/hooks/use-user-workspace-config.ts +75 -0
  221. package/src/hooks/use-workspace-currency.ts +8 -3
  222. package/src/hooks/useBoardRealtime.ts +6 -3
  223. package/src/hooks/useBoardRealtime.types.ts +11 -0
  224. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
  225. package/src/hooks/useCursorTracking.ts +91 -27
  226. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { act, render, waitFor } from '@testing-library/react';
6
+ import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
7
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
8
+ import type { ListPaginationState } from '../../shared/progressive-loader-context';
9
+ import { BoardColumn } from './board-column';
10
+
11
+ const mocks = vi.hoisted(() => ({
12
+ createTask: vi.fn(),
13
+ loadListPage: vi.fn(),
14
+ pagination: {} as Record<string, ListPaginationState>,
15
+ }));
16
+
17
+ vi.mock('@dnd-kit/sortable', () => ({
18
+ useSortable: () => ({
19
+ attributes: {},
20
+ isDragging: false,
21
+ listeners: {},
22
+ setNodeRef: vi.fn(),
23
+ transform: null,
24
+ transition: null,
25
+ }),
26
+ }));
27
+
28
+ vi.mock('@dnd-kit/utilities', () => ({
29
+ CSS: {
30
+ Transform: {
31
+ toString: () => '',
32
+ },
33
+ },
34
+ }));
35
+
36
+ vi.mock('next-intl', () => ({
37
+ useTranslations: () => (key: string, values?: Record<string, string>) =>
38
+ values?.name ? `${key}:${values.name}` : key,
39
+ }));
40
+
41
+ vi.mock('../../hooks/useTaskDialog', () => ({
42
+ useTaskDialog: () => ({
43
+ createTask: mocks.createTask,
44
+ }),
45
+ }));
46
+
47
+ vi.mock('../../shared/progressive-loader-context', () => ({
48
+ useProgressiveLoader: () => ({
49
+ loadListPage: mocks.loadListPage,
50
+ pagination: mocks.pagination,
51
+ revalidateLoadedLists: vi.fn(),
52
+ }),
53
+ }));
54
+
55
+ vi.mock('./list-actions', () => ({
56
+ ListActions: () => <div data-testid="list-actions" />,
57
+ }));
58
+
59
+ vi.mock('./task-list', () => ({
60
+ VirtualizedTaskList: () => <div data-testid="task-list" />,
61
+ }));
62
+
63
+ const externalColumn: TaskList = {
64
+ archived: false,
65
+ board_id: 'board-1',
66
+ color: 'CYAN',
67
+ created_at: '2026-06-16T00:00:00.000Z',
68
+ creator_id: 'user-1',
69
+ deleted: false,
70
+ id: 'personal-external-staging:board-1',
71
+ is_external_staging: true,
72
+ name: 'External tasks',
73
+ position: 0,
74
+ status: 'active',
75
+ };
76
+
77
+ const loadedExternalState: ListPaginationState = {
78
+ hasMore: true,
79
+ isInitialLoad: false,
80
+ isLoading: false,
81
+ page: 0,
82
+ totalCount: 0,
83
+ };
84
+
85
+ function renderExternalColumn() {
86
+ return (
87
+ <BoardColumn
88
+ boardId="board-1"
89
+ column={externalColumn}
90
+ tasks={[]}
91
+ wsId="personal"
92
+ />
93
+ );
94
+ }
95
+
96
+ describe('BoardColumn external lane retry behavior', () => {
97
+ beforeEach(() => {
98
+ vi.clearAllMocks();
99
+ mocks.pagination = {
100
+ [externalColumn.id]: loadedExternalState,
101
+ };
102
+ mocks.loadListPage.mockRejectedValue(
103
+ new Error('external lane unavailable')
104
+ );
105
+ });
106
+
107
+ it('does not immediately retry the same failed external-options signature', async () => {
108
+ const { rerender } = render(renderExternalColumn());
109
+
110
+ await waitFor(() => {
111
+ expect(mocks.loadListPage).toHaveBeenCalledTimes(1);
112
+ });
113
+
114
+ await act(async () => {
115
+ await Promise.resolve();
116
+ rerender(renderExternalColumn());
117
+ await Promise.resolve();
118
+ });
119
+
120
+ expect(mocks.loadListPage).toHaveBeenCalledTimes(1);
121
+ expect(mocks.loadListPage).toHaveBeenCalledWith(externalColumn.id, 0, {
122
+ externalIncludeDocuments: false,
123
+ externalIncludeDoneClosed: false,
124
+ externalSortBy: 'created-desc',
125
+ });
126
+ });
127
+ });
@@ -11,6 +11,8 @@ import {
11
11
  Filter,
12
12
  GripVertical,
13
13
  Loader2,
14
+ Pin,
15
+ PinOff,
14
16
  RotateCcw,
15
17
  } from '@tuturuuu/icons';
16
18
  import type { ExternalTaskSortBy } from '@tuturuuu/internal-api/tasks';
@@ -174,6 +176,9 @@ interface BoardColumnProps {
174
176
  wsId: string;
175
177
  onExternalTasksCollapsedChange?: (collapsed: boolean) => void;
176
178
  onTaskListCollapsedChange?: (listId: string, collapsed: boolean) => void;
179
+ specialPinned?: boolean;
180
+ onSpecialPinnedChange?: (pinned: boolean) => void;
181
+ readOnly?: boolean;
177
182
  }
178
183
 
179
184
  export function BoardColumn({
@@ -200,6 +205,9 @@ export function BoardColumn({
200
205
  wsId,
201
206
  onExternalTasksCollapsedChange,
202
207
  onTaskListCollapsedChange,
208
+ specialPinned = false,
209
+ onSpecialPinnedChange,
210
+ readOnly = false,
203
211
  }: BoardColumnProps) {
204
212
  const t = useTranslations('common');
205
213
  const tTasks = useTranslations('ws-tasks');
@@ -210,7 +218,9 @@ export function BoardColumn({
210
218
  const isExternalCollapsed =
211
219
  isExternalStaging && column.is_external_collapsed === true;
212
220
  const listState = pagination[column.id];
213
- const isInitialLoad = !listState || listState.isInitialLoad;
221
+ const isInitialLoad = readOnly
222
+ ? false
223
+ : !listState || listState.isInitialLoad;
214
224
  const [externalIncludeDocuments, setExternalIncludeDocuments] =
215
225
  useState(false);
216
226
  const [externalIncludeDoneClosed, setExternalIncludeDoneClosed] =
@@ -259,9 +269,7 @@ export function BoardColumn({
259
269
  loadedExternalOptionsSignatureRef.current = externalOptionsSignature;
260
270
  const promise = loadListPage(column.id, page, externalLoadOptions);
261
271
 
262
- promise.catch(() => {
263
- loadedExternalOptionsSignatureRef.current = null;
264
- });
272
+ void promise.catch(() => {});
265
273
 
266
274
  return promise;
267
275
  },
@@ -378,7 +386,7 @@ export function BoardColumn({
378
386
  isDragging,
379
387
  } = useSortable({
380
388
  id: column.id,
381
- disabled: isExternalStaging,
389
+ disabled: readOnly || isExternalStaging,
382
390
  data: {
383
391
  type: 'Column',
384
392
  column: {
@@ -448,6 +456,9 @@ export function BoardColumn({
448
456
  : visibleTasks.length;
449
457
  const externalFilterCount =
450
458
  (externalIncludeDocuments ? 1 : 0) + (externalIncludeDoneClosed ? 1 : 0);
459
+ const pinListLabel = specialPinned
460
+ ? tTasks('unpin_task_list', { name: translateListName(column.name) })
461
+ : tTasks('pin_task_list', { name: translateListName(column.name) });
451
462
 
452
463
  // Memoize drag handle for performance
453
464
  const DragHandle = useMemo(
@@ -503,6 +514,8 @@ export function BoardColumn({
503
514
  <Card
504
515
  ref={composedRef}
505
516
  style={style}
517
+ data-kanban-column-id={column.id}
518
+ data-kanban-real-column={isExternalStaging ? undefined : 'true'}
506
519
  className={cn(
507
520
  'group flex h-full w-14 shrink-0 snap-start flex-col items-center rounded-xl border border-dashed transition-all duration-200',
508
521
  'touch-none select-none overflow-hidden hover:shadow-md',
@@ -552,6 +565,8 @@ export function BoardColumn({
552
565
  <Card
553
566
  ref={composedRef}
554
567
  style={style}
568
+ data-kanban-column-id={column.id}
569
+ data-kanban-real-column={isExternalStaging ? undefined : 'true'}
555
570
  className={cn(
556
571
  'group flex h-full w-[var(--kanban-column-width)] shrink-0 snap-start flex-col rounded-xl transition-all duration-200 last:snap-end',
557
572
  'touch-none select-none',
@@ -568,7 +583,7 @@ export function BoardColumn({
568
583
  )}
569
584
  >
570
585
  <div className="flex items-center gap-2 rounded-t-xl border-b p-3">
571
- {!isExternalStaging && DragHandle}
586
+ {!readOnly && !isExternalStaging && DragHandle}
572
587
  <div className="flex flex-1 items-center gap-2">
573
588
  <span className="text-sm">{statusIcon}</span>
574
589
  <h3
@@ -579,9 +594,9 @@ export function BoardColumn({
579
594
  : 'cursor-pointer hover:underline'
580
595
  )}
581
596
  onClick={() => {
582
- if (!isExternalStaging) setIsEditOpen(true);
597
+ if (!readOnly && !isExternalStaging) setIsEditOpen(true);
583
598
  }}
584
- title={isExternalStaging ? undefined : t('edit_list')}
599
+ title={readOnly || isExternalStaging ? undefined : t('edit_list')}
585
600
  >
586
601
  {translateListName(column.name)}
587
602
  </h3>
@@ -707,53 +722,104 @@ export function BoardColumn({
707
722
  </DropdownMenuRadioGroup>
708
723
  </DropdownMenuContent>
709
724
  </DropdownMenu>
710
- <Button
711
- type="button"
712
- variant="ghost"
713
- size="xs"
714
- className="h-7 w-7 p-0 text-dynamic-cyan hover:bg-dynamic-cyan/10"
715
- title={tTasks('collapse_external_tasks')}
716
- aria-label={tTasks('collapse_external_tasks')}
717
- onClick={() => onExternalTasksCollapsedChange?.(true)}
718
- >
719
- <ChevronLeft className="h-3.5 w-3.5" />
720
- </Button>
721
- </>
722
- ) : (
723
- <>
724
- {isClosedCollapsed || column.status === 'closed' ? (
725
+ {onSpecialPinnedChange ? (
725
726
  <Button
726
727
  type="button"
727
728
  variant="ghost"
728
729
  size="xs"
729
730
  className={cn(
730
- 'h-7 w-7 p-0 hover:bg-muted/40',
731
- getListTextColorClass(column.color as SupportedColor)
731
+ 'h-7 w-7 p-0 text-dynamic-cyan hover:bg-dynamic-cyan/10',
732
+ specialPinned && 'bg-dynamic-cyan/10'
732
733
  )}
733
- title={tTasks('collapse_task_list', {
734
- name: translateListName(column.name),
735
- })}
736
- aria-label={tTasks('collapse_task_list', {
737
- name: translateListName(column.name),
738
- })}
739
- onClick={() => onTaskListCollapsedChange?.(column.id, true)}
734
+ title={pinListLabel}
735
+ aria-label={pinListLabel}
736
+ onClick={() => onSpecialPinnedChange(!specialPinned)}
737
+ >
738
+ {specialPinned ? (
739
+ <PinOff className="h-3.5 w-3.5" />
740
+ ) : (
741
+ <Pin className="h-3.5 w-3.5" />
742
+ )}
743
+ </Button>
744
+ ) : null}
745
+ {!specialPinned && (
746
+ <Button
747
+ type="button"
748
+ variant="ghost"
749
+ size="xs"
750
+ className="h-7 w-7 p-0 text-dynamic-cyan hover:bg-dynamic-cyan/10"
751
+ title={tTasks('collapse_external_tasks')}
752
+ aria-label={tTasks('collapse_external_tasks')}
753
+ onClick={() => onExternalTasksCollapsedChange?.(true)}
740
754
  >
741
755
  <ChevronLeft className="h-3.5 w-3.5" />
742
756
  </Button>
757
+ )}
758
+ </>
759
+ ) : (
760
+ <>
761
+ {isClosedCollapsed || column.status === 'closed' ? (
762
+ <>
763
+ {onSpecialPinnedChange ? (
764
+ <Button
765
+ type="button"
766
+ variant="ghost"
767
+ size="xs"
768
+ className={cn(
769
+ 'h-7 w-7 p-0 hover:bg-muted/40',
770
+ getListTextColorClass(column.color as SupportedColor),
771
+ specialPinned && 'bg-muted/40'
772
+ )}
773
+ title={pinListLabel}
774
+ aria-label={pinListLabel}
775
+ onClick={() => onSpecialPinnedChange(!specialPinned)}
776
+ >
777
+ {specialPinned ? (
778
+ <PinOff className="h-3.5 w-3.5" />
779
+ ) : (
780
+ <Pin className="h-3.5 w-3.5" />
781
+ )}
782
+ </Button>
783
+ ) : null}
784
+ {!specialPinned && (
785
+ <Button
786
+ type="button"
787
+ variant="ghost"
788
+ size="xs"
789
+ className={cn(
790
+ 'h-7 w-7 p-0 hover:bg-muted/40',
791
+ getListTextColorClass(column.color as SupportedColor)
792
+ )}
793
+ title={tTasks('collapse_task_list', {
794
+ name: translateListName(column.name),
795
+ })}
796
+ aria-label={tTasks('collapse_task_list', {
797
+ name: translateListName(column.name),
798
+ })}
799
+ onClick={() =>
800
+ onTaskListCollapsedChange?.(column.id, true)
801
+ }
802
+ >
803
+ <ChevronLeft className="h-3.5 w-3.5" />
804
+ </Button>
805
+ )}
806
+ </>
743
807
  ) : null}
744
- <ListActions
745
- listId={column.id}
746
- listName={column.name}
747
- listStatus={column.status}
748
- listColor={column.color as SupportedColor}
749
- tasks={tasks}
750
- boardId={boardId}
751
- wsId={wsId}
752
- onUpdate={handleUpdate}
753
- onSelectAll={handleSelectAll}
754
- isEditOpen={isEditOpen}
755
- onEditOpenChange={setIsEditOpen}
756
- />
808
+ {!readOnly && (
809
+ <ListActions
810
+ listId={column.id}
811
+ listName={column.name}
812
+ listStatus={column.status}
813
+ listColor={column.color as SupportedColor}
814
+ tasks={tasks}
815
+ boardId={boardId}
816
+ wsId={wsId}
817
+ onUpdate={handleUpdate}
818
+ onSelectAll={handleSelectAll}
819
+ isEditOpen={isEditOpen}
820
+ onEditOpenChange={setIsEditOpen}
821
+ />
822
+ )}
757
823
  </>
758
824
  )}
759
825
  </div>
@@ -785,10 +851,11 @@ export function BoardColumn({
785
851
  onLoadMore={handleLoadMore}
786
852
  hasMore={listState?.hasMore ?? false}
787
853
  isLoadingMore={listState?.isLoading ?? false}
854
+ readOnly={readOnly}
788
855
  />
789
856
  )}
790
857
 
791
- {!isExternalStaging && (
858
+ {!readOnly && !isExternalStaging && (
792
859
  <div className="rounded-b-xl border-t p-3 backdrop-blur-sm">
793
860
  <Button
794
861
  variant="ghost"
@@ -4,6 +4,7 @@ import { type QueryClient, useMutation } from '@tanstack/react-query';
4
4
  import { bulkWorkspaceTasks } from '@tuturuuu/internal-api/tasks';
5
5
  import { toast } from '@tuturuuu/ui/sonner';
6
6
  import type { BoardBroadcastFn } from '../../../../shared/board-broadcast-context';
7
+ import { invalidateKanbanDeadlineTasks } from '../data/kanban-deadline-query';
7
8
  import type { BulkOperationI18n } from './bulk-operation-i18n';
8
9
  import {
9
10
  type BulkTaskWorkspaceGroup,
@@ -390,6 +391,7 @@ export function useBulkDeleteTasks(
390
391
  for (const tid of succeededTaskIds) {
391
392
  broadcast?.('task:delete', { taskId: tid });
392
393
  }
394
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
393
395
 
394
396
  clearSelection();
395
397
  setBulkDeleteOpen(false);
@@ -6,6 +6,7 @@ import type { Task } from '@tuturuuu/types/primitives/Task';
6
6
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
7
7
  import { toast } from '@tuturuuu/ui/sonner';
8
8
  import type { BoardBroadcastFn } from '../../../../shared/board-broadcast-context';
9
+ import { invalidateKanbanDeadlineTasks } from '../data/kanban-deadline-query';
9
10
  import type { BulkOperationI18n } from './bulk-operation-i18n';
10
11
  import { getInternalApiOptions } from './bulk-operation-utils';
11
12
  import {
@@ -184,6 +185,8 @@ export function useBulkMoveToBoard(
184
185
  for (const tid of movedTaskIds) {
185
186
  broadcast?.('task:delete', { taskId: tid });
186
187
  }
188
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
189
+ void invalidateKanbanDeadlineTasks(queryClient, data.targetBoardId);
187
190
 
188
191
  if (data.failures.length > 0) {
189
192
  toast.warning(
@@ -428,6 +431,7 @@ export function useBulkMoveToList(
428
431
  },
429
432
  });
430
433
  }
434
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
431
435
 
432
436
  if (data.failures.length > 0) {
433
437
  toast.warning(
@@ -631,6 +635,7 @@ export function useBulkMoveToStatus(
631
635
  },
632
636
  });
633
637
  }
638
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
634
639
 
635
640
  if (data.failures.length > 0) {
636
641
  toast.warning(
@@ -4,6 +4,7 @@ import { type QueryClient, useMutation } from '@tanstack/react-query';
4
4
  import type { Task } from '@tuturuuu/types/primitives/Task';
5
5
  import { toast } from '@tuturuuu/ui/sonner';
6
6
  import type { BoardBroadcastFn } from '../../../../shared/board-broadcast-context';
7
+ import { invalidateKanbanDeadlineTasks } from '../data/kanban-deadline-query';
7
8
  import type { BulkOperationI18n } from './bulk-operation-i18n';
8
9
  import {
9
10
  bulkWorkspaceTasksByEffectiveWorkspace,
@@ -322,6 +323,7 @@ export function useBulkUpdateDueDate(
322
323
  task: { id: tid, end_date: data.end_date },
323
324
  });
324
325
  }
326
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
325
327
 
326
328
  if (data.failures.length > 0) {
327
329
  toast.warning(
@@ -434,6 +436,7 @@ export function useBulkUpdateCustomDueDate(
434
436
  task: { id: tid, end_date: data.end_date },
435
437
  });
436
438
  }
439
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
437
440
 
438
441
  if (data.failures.length > 0) {
439
442
  toast.warning(
@@ -1,22 +1,70 @@
1
+ import type { QueryClient } from '@tanstack/react-query';
1
2
  import { listWorkspaceTasks } from '@tuturuuu/internal-api';
3
+ import type { ListWorkspaceTasksOptions } from '@tuturuuu/internal-api/tasks';
2
4
  import type { Task } from '@tuturuuu/types/primitives/Task';
3
5
 
4
6
  const KANBAN_DEADLINE_TASK_PAGE_SIZE = 200;
7
+ export const KANBAN_DEADLINE_TASKS_QUERY_KEY = 'kanban-deadline-tasks';
8
+
9
+ export function getKanbanDeadlineTasksQueryKey(
10
+ workspaceId: string,
11
+ boardId: string | null | undefined,
12
+ taskQueryOptions?: ListWorkspaceTasksOptions
13
+ ) {
14
+ return [
15
+ KANBAN_DEADLINE_TASKS_QUERY_KEY,
16
+ workspaceId,
17
+ boardId,
18
+ taskQueryOptions,
19
+ ] as const;
20
+ }
21
+
22
+ export function invalidateKanbanDeadlineTasks(
23
+ queryClient: QueryClient,
24
+ boardId?: string | null
25
+ ) {
26
+ return queryClient.invalidateQueries({
27
+ predicate: (query) => {
28
+ const queryKey = query.queryKey;
29
+ if (!Array.isArray(queryKey)) return false;
30
+ if (queryKey[0] !== KANBAN_DEADLINE_TASKS_QUERY_KEY) return false;
31
+ return !boardId || queryKey[2] === boardId;
32
+ },
33
+ });
34
+ }
5
35
 
6
36
  interface ListKanbanDeadlineTasksOptions {
7
37
  boardId: string;
38
+ taskQueryOptions?: ListWorkspaceTasksOptions;
8
39
  workspaceId: string;
9
40
  }
10
41
 
11
42
  export async function listKanbanDeadlineTasks({
12
43
  boardId,
44
+ taskQueryOptions,
13
45
  workspaceId,
14
46
  }: ListKanbanDeadlineTasksOptions): Promise<Task[]> {
15
47
  const tasks: Task[] = [];
16
48
  let offset = 0;
49
+ const {
50
+ boardId: _boardId,
51
+ closed: _closed,
52
+ completed: _completed,
53
+ externalSortBy: _externalSortBy,
54
+ hasDueDate: _hasDueDate,
55
+ includeCount: _includeCount,
56
+ includeListCounts: _includeListCounts,
57
+ includeRelationshipSummary: _includeRelationshipSummary,
58
+ limit: _limit,
59
+ listId: _listId,
60
+ offset: _offset,
61
+ sortBy: _sortBy,
62
+ ...filterOptions
63
+ } = taskQueryOptions ?? {};
17
64
 
18
65
  while (true) {
19
66
  const response = await listWorkspaceTasks(workspaceId, {
67
+ ...filterOptions,
20
68
  boardId,
21
69
  closed: 'exclude',
22
70
  completed: 'exclude',
@@ -25,9 +73,9 @@ export async function listKanbanDeadlineTasks({
25
73
  includeCount: true,
26
74
  includeRelationshipSummary: false,
27
75
  limit: KANBAN_DEADLINE_TASK_PAGE_SIZE,
28
- listStatuses: ['not_started', 'active'],
76
+ listStatuses: filterOptions.listStatuses ?? ['not_started', 'active'],
29
77
  offset,
30
- sourceScope: 'all_visible',
78
+ sourceScope: filterOptions.sourceScope ?? 'all_visible',
31
79
  });
32
80
 
33
81
  tasks.push(...response.tasks);
@@ -43,6 +43,23 @@ describe('getColumnReorderUpdates', () => {
43
43
  expect(getColumnReorderUpdates(columns, 'todo', 'doing')).toBeNull();
44
44
  });
45
45
 
46
+ it('excludes synthetic external staging lanes from persisted position repairs', () => {
47
+ const externalLane = {
48
+ ...makeList('external', 'not_started', -1),
49
+ is_external_staging: true,
50
+ };
51
+ const columns = [
52
+ externalLane,
53
+ makeList('todo', 'not_started', 0),
54
+ makeList('backlog', 'not_started', 1),
55
+ ];
56
+
57
+ expect(getColumnReorderUpdates(columns, 'backlog', 'todo')).toEqual([
58
+ { listId: 'backlog', newPosition: 0 },
59
+ { listId: 'todo', newPosition: 1 },
60
+ ]);
61
+ });
62
+
46
63
  it('sorts columns in the same order they are rendered in Kanban', () => {
47
64
  const columns = [
48
65
  makeList('closed', 'closed', 0),
@@ -39,7 +39,10 @@ export function getColumnReorderUpdates(
39
39
  }
40
40
 
41
41
  const statusColumns = [...columns]
42
- .filter((column) => column.status === activeColumn.status)
42
+ .filter(
43
+ (column) =>
44
+ column.status === activeColumn.status && !column.is_external_staging
45
+ )
43
46
  .sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
44
47
 
45
48
  const activeIndex = statusColumns.findIndex(