@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,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
+ 'w-full overflow-hidden bg-transparent',
18
+ root
19
+ ? '-m-4 h-[calc(100dvh+2rem)] min-h-[calc(32rem+2rem)]'
20
+ : 'h-[calc(100dvh-1rem)] min-h-[32rem]',
21
+ className
22
+ )}
23
+ data-testid="task-board-loading-state"
24
+ >
25
+ <KanbanSkeleton />
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveTaskDialogOpeningPresentation } from './task-dialog-presentation';
3
+
4
+ describe('resolveTaskDialogOpeningPresentation', () => {
5
+ it('opens existing document-list tasks fullscreen', () => {
6
+ expect(
7
+ resolveTaskDialogOpeningPresentation({
8
+ defaultPresentation: 'compact',
9
+ mode: 'edit',
10
+ selectedListStatus: 'documents',
11
+ })
12
+ ).toBe('fullscreen');
13
+ });
14
+
15
+ it('keeps create mode compact even in document lists', () => {
16
+ expect(
17
+ resolveTaskDialogOpeningPresentation({
18
+ defaultPresentation: 'fullscreen',
19
+ mode: 'create',
20
+ selectedListStatus: 'documents',
21
+ })
22
+ ).toBe('compact');
23
+ });
24
+
25
+ it('respects the user default for existing non-document tasks', () => {
26
+ expect(
27
+ resolveTaskDialogOpeningPresentation({
28
+ defaultPresentation: 'compact',
29
+ mode: 'edit',
30
+ selectedListStatus: 'active',
31
+ })
32
+ ).toBe('compact');
33
+
34
+ expect(
35
+ resolveTaskDialogOpeningPresentation({
36
+ defaultPresentation: 'fullscreen',
37
+ mode: 'edit',
38
+ selectedListStatus: 'not_started',
39
+ })
40
+ ).toBe('fullscreen');
41
+ });
42
+
43
+ it('keeps drafts fullscreen', () => {
44
+ expect(
45
+ resolveTaskDialogOpeningPresentation({
46
+ defaultPresentation: 'compact',
47
+ draftId: 'draft-1',
48
+ mode: 'create',
49
+ selectedListStatus: 'documents',
50
+ })
51
+ ).toBe('fullscreen');
52
+ });
53
+ });
@@ -2,6 +2,7 @@ export const TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID =
2
2
  'TASK_DIALOG_DEFAULT_PRESENTATION';
3
3
 
4
4
  export type TaskDialogPresentation = 'compact' | 'fullscreen';
5
+ export type TaskDialogMode = 'edit' | 'create';
5
6
 
6
7
  export function normalizeTaskDialogPresentation(
7
8
  value: unknown,
@@ -9,3 +10,21 @@ export function normalizeTaskDialogPresentation(
9
10
  ): TaskDialogPresentation {
10
11
  return value === 'fullscreen' || value === 'compact' ? value : fallback;
11
12
  }
13
+
14
+ export function resolveTaskDialogOpeningPresentation({
15
+ defaultPresentation,
16
+ draftId,
17
+ mode = 'edit',
18
+ selectedListStatus,
19
+ }: {
20
+ defaultPresentation?: unknown;
21
+ draftId?: string;
22
+ mode?: TaskDialogMode;
23
+ selectedListStatus?: string | null;
24
+ }): TaskDialogPresentation {
25
+ if (draftId) return 'fullscreen';
26
+ if (mode === 'create') return 'compact';
27
+ if (selectedListStatus === 'documents') return 'fullscreen';
28
+
29
+ return normalizeTaskDialogPresentation(defaultPresentation);
30
+ }
@@ -178,6 +178,63 @@ describe('CompactTaskCreatePopover', () => {
178
178
  ).not.toBeInTheDocument();
179
179
  });
180
180
 
181
+ it('renders compact description preview without affecting panel layout', () => {
182
+ const onDescriptionPreviewClick = vi.fn();
183
+
184
+ render(
185
+ <Dialog open={true}>
186
+ <CompactTaskCreatePopover
187
+ title="Edit task"
188
+ titleInput={<input aria-label="Task title" defaultValue="Existing" />}
189
+ propertyControls={
190
+ <button type="button" aria-label="List: Inbox">
191
+ List
192
+ </button>
193
+ }
194
+ descriptionPreview="Confirm the plan and publish the final notes."
195
+ descriptionPreviewLabel="Open full task"
196
+ onDescriptionPreviewClick={onDescriptionPreviewClick}
197
+ onClose={vi.fn()}
198
+ onFullscreen={vi.fn()}
199
+ />
200
+ </Dialog>
201
+ );
202
+
203
+ const preview = screen.getByTestId('compact-task-description-preview');
204
+
205
+ expect(preview).toHaveTextContent(
206
+ 'Confirm the plan and publish the final notes.'
207
+ );
208
+ expect(preview).toHaveAttribute('aria-label', 'Open full task');
209
+ expect(preview).toHaveClass('absolute', 'top-full');
210
+
211
+ fireEvent.click(preview);
212
+
213
+ expect(onDescriptionPreviewClick).toHaveBeenCalledTimes(1);
214
+ });
215
+
216
+ it('omits compact description preview when the caller does not provide one', () => {
217
+ render(
218
+ <Dialog open={true}>
219
+ <CompactTaskCreatePopover
220
+ title="Create task"
221
+ titleInput={<input aria-label="Task title" defaultValue="New" />}
222
+ propertyControls={
223
+ <button type="button" aria-label="List: Inbox">
224
+ List
225
+ </button>
226
+ }
227
+ onClose={vi.fn()}
228
+ onFullscreen={vi.fn()}
229
+ />
230
+ </Dialog>
231
+ );
232
+
233
+ expect(
234
+ screen.queryByTestId('compact-task-description-preview')
235
+ ).not.toBeInTheDocument();
236
+ });
237
+
181
238
  it('renders compact edit actions when provided', () => {
182
239
  const onDelete = vi.fn();
183
240
  const onDone = vi.fn();
@@ -25,6 +25,8 @@ interface CompactTaskDialogPanelProps {
25
25
  iconRingClass?: string;
26
26
  titleInput: ReactNode;
27
27
  showHeaderTitle?: boolean;
28
+ descriptionPreview?: string | null;
29
+ descriptionPreviewLabel?: string;
28
30
  taskStatus?: ReactNode;
29
31
  propertyControls: ReactNode;
30
32
  editActions?: ReactNode;
@@ -39,6 +41,7 @@ interface CompactTaskDialogPanelProps {
39
41
  onCreateMultipleChange?: (value: boolean) => void;
40
42
  onClose: () => void;
41
43
  onFullscreen: () => void;
44
+ onDescriptionPreviewClick?: () => void;
42
45
  onSave?: () => void;
43
46
  }
44
47
 
@@ -84,6 +87,8 @@ export function CompactTaskDialogPanel({
84
87
  iconRingClass = 'ring-dynamic-orange/20',
85
88
  titleInput,
86
89
  showHeaderTitle = true,
90
+ descriptionPreview,
91
+ descriptionPreviewLabel,
87
92
  taskStatus,
88
93
  propertyControls,
89
94
  editActions,
@@ -98,6 +103,7 @@ export function CompactTaskDialogPanel({
98
103
  onCreateMultipleChange,
99
104
  onClose,
100
105
  onFullscreen,
106
+ onDescriptionPreviewClick,
101
107
  onSave,
102
108
  }: CompactTaskDialogPanelProps) {
103
109
  const t = useTranslations();
@@ -114,127 +120,146 @@ export function CompactTaskDialogPanel({
114
120
  const hasHeaderTitle = showHeaderTitle;
115
121
 
116
122
  return (
117
- <div
118
- data-testid="compact-task-dialog-panel"
119
- className="flex max-h-[calc(100vh-2rem)] min-h-0 flex-col overflow-hidden rounded-lg bg-background"
120
- >
123
+ <div className="relative">
121
124
  <div
122
- className={cn(
123
- 'flex items-start gap-3 border-b px-4 py-3',
124
- hasHeaderTitle ? 'justify-between' : 'justify-end'
125
- )}
125
+ data-testid="compact-task-dialog-panel"
126
+ className="flex max-h-[calc(100vh-2rem)] min-h-0 flex-col overflow-hidden rounded-lg bg-background"
126
127
  >
127
- {hasHeaderTitle ? (
128
- <div className="flex min-w-0 items-start gap-2.5">
129
- <div
130
- className={cn(
131
- 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ring-1',
132
- iconBgClass,
133
- iconRingClass
134
- )}
135
- >
136
- {icon ?? <ListTodo className="h-4 w-4 text-dynamic-orange" />}
137
- </div>
138
- <div className="min-w-0 space-y-0.5">
139
- <DialogTitle className="truncate font-semibold text-base">
140
- {title}
141
- </DialogTitle>
142
- {description && (
143
- <DialogDescription className="truncate text-muted-foreground text-xs">
144
- {description}
145
- </DialogDescription>
146
- )}
128
+ <div
129
+ className={cn(
130
+ 'flex items-start gap-3 border-b px-4 py-3',
131
+ hasHeaderTitle ? 'justify-between' : 'justify-end'
132
+ )}
133
+ >
134
+ {hasHeaderTitle ? (
135
+ <div className="flex min-w-0 items-start gap-2.5">
136
+ <div
137
+ className={cn(
138
+ 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ring-1',
139
+ iconBgClass,
140
+ iconRingClass
141
+ )}
142
+ >
143
+ {icon ?? <ListTodo className="h-4 w-4 text-dynamic-orange" />}
144
+ </div>
145
+ <div className="min-w-0 space-y-0.5">
146
+ <DialogTitle className="truncate font-semibold text-base">
147
+ {title}
148
+ </DialogTitle>
149
+ {description && (
150
+ <DialogDescription className="truncate text-muted-foreground text-xs">
151
+ {description}
152
+ </DialogDescription>
153
+ )}
154
+ </div>
147
155
  </div>
156
+ ) : (
157
+ <DialogTitle className="sr-only">{title}</DialogTitle>
158
+ )}
159
+ <div className="flex shrink-0 items-center gap-1">
160
+ {smartAction}
161
+ {editActions}
162
+ <Tooltip>
163
+ <TooltipTrigger asChild>
164
+ <Button
165
+ type="button"
166
+ variant="ghost"
167
+ size="icon"
168
+ aria-label={t('ws-task-boards.dialog.open_fullscreen')}
169
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
170
+ onClick={onFullscreen}
171
+ >
172
+ <Maximize2 className="h-4 w-4" />
173
+ </Button>
174
+ </TooltipTrigger>
175
+ <TooltipContent side="bottom">
176
+ {t('ws-task-boards.dialog.open_fullscreen')}
177
+ </TooltipContent>
178
+ </Tooltip>
179
+ <Tooltip>
180
+ <TooltipTrigger asChild>
181
+ <Button
182
+ type="button"
183
+ variant="ghost"
184
+ size="icon"
185
+ aria-label={t('common.close')}
186
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
187
+ onClick={onClose}
188
+ >
189
+ <X className="h-4 w-4" />
190
+ </Button>
191
+ </TooltipTrigger>
192
+ <TooltipContent side="bottom">{t('common.close')}</TooltipContent>
193
+ </Tooltip>
148
194
  </div>
149
- ) : (
150
- <DialogTitle className="sr-only">{title}</DialogTitle>
151
- )}
152
- <div className="flex shrink-0 items-center gap-1">
153
- {smartAction}
154
- {editActions}
155
- <Tooltip>
156
- <TooltipTrigger asChild>
157
- <Button
158
- type="button"
159
- variant="ghost"
160
- size="icon"
161
- aria-label={t('ws-task-boards.dialog.open_fullscreen')}
162
- className="h-8 w-8 text-muted-foreground hover:text-foreground"
163
- onClick={onFullscreen}
164
- >
165
- <Maximize2 className="h-4 w-4" />
166
- </Button>
167
- </TooltipTrigger>
168
- <TooltipContent side="bottom">
169
- {t('ws-task-boards.dialog.open_fullscreen')}
170
- </TooltipContent>
171
- </Tooltip>
172
- <Tooltip>
173
- <TooltipTrigger asChild>
174
- <Button
175
- type="button"
176
- variant="ghost"
177
- size="icon"
178
- aria-label={t('common.close')}
179
- className="h-8 w-8 text-muted-foreground hover:text-foreground"
180
- onClick={onClose}
181
- >
182
- <X className="h-4 w-4" />
183
- </Button>
184
- </TooltipTrigger>
185
- <TooltipContent side="bottom">{t('common.close')}</TooltipContent>
186
- </Tooltip>
187
195
  </div>
188
- </div>
189
196
 
190
- <div className="min-h-0 space-y-3 overflow-y-auto px-4 py-3">
191
- {titleInput}
192
- {taskStatus}
193
- <div className="flex flex-wrap items-center gap-1.5">
194
- {propertyControls}
197
+ <div className="min-h-0 space-y-3 overflow-y-auto px-4 py-3">
198
+ {titleInput}
199
+ {taskStatus}
200
+ <div className="flex flex-wrap items-center gap-1.5">
201
+ {propertyControls}
202
+ </div>
203
+ {smartPanel}
195
204
  </div>
196
- {smartPanel}
197
- </div>
198
205
 
199
- {hasCreateActions && (
200
- <div className="flex items-center justify-between gap-2 border-t bg-muted/20 px-4 py-3">
201
- <div className="flex items-center gap-1">
202
- <CompactIconButton
203
- active={!!saveAsDraft}
204
- label={t('task-drafts.save_as_draft')}
205
- onClick={() => onSaveAsDraftChange?.(!saveAsDraft)}
206
- >
207
- <FileEdit className="h-4 w-4" />
208
- </CompactIconButton>
209
- <CompactIconButton
210
- active={!!createMultiple}
211
- label={t('ws-task-boards.dialog.create_multiple')}
212
- onClick={() => onCreateMultipleChange?.(!createMultiple)}
206
+ {hasCreateActions && (
207
+ <div className="flex items-center justify-between gap-2 border-t bg-muted/20 px-4 py-3">
208
+ <div className="flex items-center gap-1">
209
+ <CompactIconButton
210
+ active={!!saveAsDraft}
211
+ label={t('task-drafts.save_as_draft')}
212
+ onClick={() => onSaveAsDraftChange?.(!saveAsDraft)}
213
+ >
214
+ <FileEdit className="h-4 w-4" />
215
+ </CompactIconButton>
216
+ <CompactIconButton
217
+ active={!!createMultiple}
218
+ label={t('ws-task-boards.dialog.create_multiple')}
219
+ onClick={() => onCreateMultipleChange?.(!createMultiple)}
220
+ >
221
+ <Copy className="h-4 w-4" />
222
+ </CompactIconButton>
223
+ <QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
224
+ </div>
225
+ <Button
226
+ type="button"
227
+ size="sm"
228
+ disabled={!canSave}
229
+ onClick={() => onSave?.()}
230
+ className="min-w-28"
213
231
  >
214
- <Copy className="h-4 w-4" />
215
- </CompactIconButton>
216
- <QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
232
+ {isLoading ? (
233
+ <>
234
+ <Loader2 className="h-4 w-4 animate-spin" />
235
+ {t('ws-task-boards.dialog.saving')}
236
+ </>
237
+ ) : (
238
+ <>
239
+ <Check className="h-4 w-4" />
240
+ {saveLabel}
241
+ </>
242
+ )}
243
+ </Button>
217
244
  </div>
218
- <Button
219
- type="button"
220
- size="sm"
221
- disabled={!canSave}
222
- onClick={() => onSave?.()}
223
- className="min-w-28"
224
- >
225
- {isLoading ? (
226
- <>
227
- <Loader2 className="h-4 w-4 animate-spin" />
228
- {t('ws-task-boards.dialog.saving')}
229
- </>
230
- ) : (
231
- <>
232
- <Check className="h-4 w-4" />
233
- {saveLabel}
234
- </>
235
- )}
236
- </Button>
237
- </div>
245
+ )}
246
+ </div>
247
+
248
+ {descriptionPreview && onDescriptionPreviewClick && (
249
+ <button
250
+ type="button"
251
+ data-testid="compact-task-description-preview"
252
+ aria-label={
253
+ descriptionPreviewLabel ??
254
+ t('ws-task-boards.dialog.open_fullscreen')
255
+ }
256
+ className="absolute top-full left-1/2 mt-2 w-full max-w-[30rem] -translate-x-1/2 rounded-lg border bg-background/95 px-4 py-3 text-left opacity-70 shadow-xl ring-1 ring-border/60 backdrop-blur transition hover:bg-muted/70 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
257
+ onClick={onDescriptionPreviewClick}
258
+ >
259
+ <span className="line-clamp-3 whitespace-pre-line text-muted-foreground text-sm leading-relaxed">
260
+ {descriptionPreview}
261
+ </span>
262
+ </button>
238
263
  )}
239
264
  </div>
240
265
  );
@@ -4,6 +4,7 @@ import type { QueryClient } from '@tanstack/react-query';
4
4
  import type { Editor, JSONContent } from '@tiptap/react';
5
5
  import { Loader2 } from '@tuturuuu/icons';
6
6
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
7
+ import { getBoardRealtimeChannelName } from '@tuturuuu/ui/hooks/useBoardRealtime.types';
7
8
  import { toast } from '@tuturuuu/ui/sonner';
8
9
  import { cn } from '@tuturuuu/utils/format';
9
10
  import { useTranslations } from 'next-intl';
@@ -352,8 +353,9 @@ export function TaskDescriptionEditor({
352
353
 
353
354
  {showCollaborationCursors && taskId && (
354
355
  <CursorOverlayMultiWrapper
355
- channelName={`task-cursor-${taskId}`}
356
+ channelName={getBoardRealtimeChannelName(boardId)}
356
357
  containerRef={richTextEditorRef}
358
+ cursorScope={{ taskId, type: 'task-description' }}
357
359
  />
358
360
  )}
359
361
 
@@ -373,8 +373,9 @@ function DescriptionDiff({
373
373
  </>
374
374
  );
375
375
 
376
- // If both exist and there are text changes, show the full diff viewer
377
- if (hasSnapshot && hasCurrent && hasTextChanges) {
376
+ // Show the full viewer whenever either side has content so users can inspect
377
+ // content-only, empty/content, and structural editor changes.
378
+ if (hasSnapshot || hasCurrent || hasTextChanges) {
378
379
  return (
379
380
  <div className="text-sm">
380
381
  <div className="flex items-center gap-2">
@@ -0,0 +1,91 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+
3
+ import { fireEvent, render, screen } from '@testing-library/react';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { SelectiveRevertPanel } from './selective-revert-panel';
6
+
7
+ vi.mock('./description-diff-viewer', () => ({
8
+ DescriptionDiffViewer: () => (
9
+ <button type="button">view-description-diff</button>
10
+ ),
11
+ }));
12
+
13
+ const t = (key: string, options?: { defaultValue?: string }) => {
14
+ const messages: Record<string, string> = {
15
+ changed: 'Changed',
16
+ 'field.description': 'Description',
17
+ 'field.name': 'Name',
18
+ 'field.priority': 'Priority',
19
+ unchanged_fields: 'Unchanged fields',
20
+ };
21
+
22
+ return messages[key] ?? options?.defaultValue ?? key;
23
+ };
24
+
25
+ const snapshot = {
26
+ assignees: [],
27
+ completed: false,
28
+ description: {
29
+ content: [
30
+ {
31
+ content: [{ text: 'Previous description', type: 'text' }],
32
+ type: 'paragraph',
33
+ },
34
+ ],
35
+ type: 'doc',
36
+ },
37
+ end_date: null,
38
+ estimation_points: null,
39
+ id: 'task-1',
40
+ labels: [],
41
+ list_id: 'list-1',
42
+ list_name: 'Review',
43
+ name: 'Previous task name',
44
+ priority: 'normal' as const,
45
+ projects: [],
46
+ start_date: null,
47
+ };
48
+
49
+ const currentTask = {
50
+ ...snapshot,
51
+ description: {
52
+ content: [
53
+ {
54
+ content: [{ text: 'Current description', type: 'text' }],
55
+ type: 'paragraph',
56
+ },
57
+ ],
58
+ type: 'doc',
59
+ },
60
+ list_name: 'Review',
61
+ name: 'Current task name',
62
+ };
63
+
64
+ describe('SelectiveRevertPanel', () => {
65
+ it('renders changed fields first and keeps unchanged fields collapsed', () => {
66
+ render(
67
+ <SelectiveRevertPanel
68
+ currentTask={currentTask}
69
+ isReverting={false}
70
+ onRevert={vi.fn()}
71
+ snapshot={snapshot}
72
+ t={t}
73
+ />
74
+ );
75
+
76
+ expect(screen.getByText('Core Fields')).toBeInTheDocument();
77
+ expect(screen.getByText('Name')).toBeInTheDocument();
78
+ expect(screen.getByText('Description')).toBeInTheDocument();
79
+ expect(screen.getByText('view-description-diff')).toBeInTheDocument();
80
+
81
+ const unchangedButton = screen.getByRole('button', {
82
+ name: /Unchanged fields/i,
83
+ });
84
+ expect(unchangedButton).toBeInTheDocument();
85
+ expect(screen.queryByText('Priority')).not.toBeInTheDocument();
86
+
87
+ fireEvent.click(unchangedButton);
88
+
89
+ expect(screen.getByText('Priority')).toBeInTheDocument();
90
+ });
91
+ });