@tuturuuu/ui 0.4.1 → 0.6.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 (107) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +41 -34
  3. package/src/components/ui/currency-input.tsx +65 -23
  4. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
  5. package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
  6. package/src/components/ui/custom/combobox.test.tsx +141 -0
  7. package/src/components/ui/custom/combobox.tsx +105 -36
  8. package/src/components/ui/custom/settings/task-settings.tsx +126 -0
  9. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
  10. package/src/components/ui/custom/sidebar-context.tsx +68 -6
  11. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
  12. package/src/components/ui/finance/finance-layout.tsx +2 -4
  13. package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
  14. package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
  15. package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
  16. package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
  17. package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
  18. package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
  19. package/src/components/ui/finance/transactions/form-types.ts +23 -0
  20. package/src/components/ui/finance/transactions/form.tsx +81 -22
  21. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
  22. package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
  23. package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
  24. package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
  25. package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
  28. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
  29. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
  30. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  31. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -0
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
  33. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
  34. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
  35. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  36. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  37. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  38. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  39. package/src/components/ui/finance/wallets/form.tsx +41 -197
  40. package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
  41. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  42. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  43. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  46. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  47. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  48. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
  49. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
  50. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  51. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  52. package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
  53. package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
  54. package/src/components/ui/storefront/accent-button.tsx +33 -0
  55. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  56. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  57. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  58. package/src/components/ui/storefront/image-panel.tsx +40 -0
  59. package/src/components/ui/storefront/index.ts +12 -0
  60. package/src/components/ui/storefront/listing-card.tsx +129 -0
  61. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  62. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  63. package/src/components/ui/storefront/types.ts +99 -0
  64. package/src/components/ui/storefront/utils.ts +90 -0
  65. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
  66. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
  67. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  68. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  69. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  70. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  71. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  72. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  73. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
  74. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  75. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
  76. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  77. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  78. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  79. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  80. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  81. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  82. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  83. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
  87. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  88. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  89. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  90. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  91. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
  92. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
  93. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
  94. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  95. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
  100. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
  101. package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
  102. package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
  103. package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
  104. package/src/hooks/use-task-actions.ts +45 -0
  105. package/src/hooks/useBoardRealtime.ts +54 -1
  106. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  107. package/src/hooks/useTaskUserRealtime.ts +338 -0
@@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
7
7
  import { ListView } from './list-view';
8
8
 
9
9
  const openTaskMock = vi.hoisted(() => vi.fn());
10
+ const openTaskByIdMock = vi.hoisted(() => vi.fn());
10
11
 
11
12
  vi.mock('next-intl', () => ({
12
13
  useLocale: () => 'en',
@@ -24,6 +25,7 @@ vi.mock('next/image', () => ({
24
25
  vi.mock('../hooks/useTaskDialog', () => ({
25
26
  useTaskDialog: () => ({
26
27
  openTask: openTaskMock,
28
+ openTaskById: openTaskByIdMock,
27
29
  }),
28
30
  }));
29
31
 
@@ -104,7 +106,7 @@ const tasks: Task[] = [
104
106
  },
105
107
  ];
106
108
 
107
- function renderListView() {
109
+ function renderListView(viewTasks = tasks) {
108
110
  const queryClient = new QueryClient({
109
111
  defaultOptions: {
110
112
  queries: { retry: false },
@@ -116,8 +118,9 @@ function renderListView() {
116
118
  <ListView
117
119
  boardId="board-1"
118
120
  lists={lists}
119
- tasks={tasks}
121
+ tasks={viewTasks}
120
122
  workspaceId="ws-1"
123
+ isPersonalWorkspace={true}
121
124
  />
122
125
  </QueryClientProvider>
123
126
  );
@@ -126,6 +129,7 @@ function renderListView() {
126
129
  describe('ListView task context menu', () => {
127
130
  beforeEach(() => {
128
131
  openTaskMock.mockReset();
132
+ openTaskByIdMock.mockReset();
129
133
  });
130
134
 
131
135
  it('opens the shared task menu from row right-click and the compact menu button', () => {
@@ -147,4 +151,53 @@ describe('ListView task context menu', () => {
147
151
  fireEvent.click(screen.getByTestId('mock-task-menu-trigger-task-1'));
148
152
  expect(screen.getByTestId('mock-task-menu-task-1')).toBeInTheDocument();
149
153
  });
154
+
155
+ it('opens external rows through the hydrating task-by-id path immediately', () => {
156
+ const externalTask: Task = {
157
+ ...tasks[0]!,
158
+ id: 'external-task',
159
+ name: 'External task',
160
+ list_id: 'personal-list',
161
+ personal_board_id: 'board-1',
162
+ is_personal_external: true,
163
+ source_workspace_id: 'source-ws',
164
+ source_board_id: 'source-board',
165
+ source_board_name: 'Source board',
166
+ source_list_id: 'source-list',
167
+ source_list_name: 'Source list',
168
+ } satisfies Task;
169
+
170
+ renderListView([externalTask]);
171
+
172
+ fireEvent.click(screen.getByText('External task'));
173
+
174
+ expect(openTaskByIdMock).toHaveBeenCalledWith(
175
+ 'external-task',
176
+ expect.objectContaining({
177
+ boardId: 'source-board',
178
+ taskWsId: 'source-ws',
179
+ taskWorkspacePersonal: false,
180
+ initialTask: expect.objectContaining({
181
+ id: 'external-task',
182
+ list_id: 'source-list',
183
+ name: 'External task',
184
+ }),
185
+ initialSharedContext: expect.objectContaining({
186
+ boardConfig: expect.objectContaining({
187
+ id: 'source-board',
188
+ name: 'Source board',
189
+ ws_id: 'source-ws',
190
+ }),
191
+ availableLists: [
192
+ expect.objectContaining({
193
+ id: 'source-list',
194
+ name: 'Source list',
195
+ board_id: 'source-board',
196
+ }),
197
+ ],
198
+ }),
199
+ })
200
+ );
201
+ expect(openTaskMock).not.toHaveBeenCalled();
202
+ });
150
203
  });
@@ -61,6 +61,10 @@ 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 {
65
+ getTaskCardHydratingOpenOptions,
66
+ isExternalTaskSnapshot,
67
+ } from '../boards/boardId/task-card/task-card-open-options';
64
68
  import { useTaskDialog } from '../hooks/useTaskDialog';
65
69
  import { computeAccessibleLabelStyles } from '../utils/label-colors';
66
70
  import { useBoardBroadcast } from './board-broadcast-context';
@@ -129,7 +133,7 @@ export function ListView({
129
133
  const [openTaskMenu, setOpenTaskMenu] = useState<TaskMenuState | null>(null);
130
134
  const previousWorkspaceIdRef = useRef(workspaceId);
131
135
  const previousBoardIdRef = useRef(boardId);
132
- const { openTask } = useTaskDialog();
136
+ const { openTask, openTaskById } = useTaskDialog();
133
137
 
134
138
  // Infinite scroll
135
139
  const [displayCount, setDisplayCount] = useState(50);
@@ -277,21 +281,24 @@ export function ListView({
277
281
  }
278
282
 
279
283
  function openTaskFromRow(task: Task) {
280
- const targetBoardId = task.source_board_id ?? boardId;
281
- const targetWorkspaceId = task.source_workspace_id ?? workspaceId;
282
-
283
- openTask(
284
- task,
285
- targetBoardId,
286
- task.source_board_id ? undefined : lists,
287
- false,
288
- {
289
- taskWsId: targetWorkspaceId,
290
- taskWorkspacePersonal: task.source_workspace_id
291
- ? false
292
- : isPersonalWorkspace,
293
- }
294
- );
284
+ if (isExternalTaskSnapshot(task)) {
285
+ void openTaskById(
286
+ task.id,
287
+ getTaskCardHydratingOpenOptions({
288
+ task,
289
+ boardId,
290
+ availableLists: lists,
291
+ effectiveWorkspaceId: workspaceId,
292
+ isPersonalWorkspace,
293
+ })
294
+ );
295
+ return;
296
+ }
297
+
298
+ openTask(task, boardId, lists, false, {
299
+ taskWsId: workspaceId,
300
+ taskWorkspacePersonal: isPersonalWorkspace,
301
+ });
295
302
  }
296
303
 
297
304
  // Infinite scroll handler
@@ -3,6 +3,7 @@
3
3
  import { useQuery, useQueryClient } from '@tanstack/react-query';
4
4
  import { getCurrentUserProfile } from '@tuturuuu/internal-api';
5
5
  import { getWorkspaceTask } from '@tuturuuu/internal-api/tasks';
6
+ import { getUserConfig } from '@tuturuuu/internal-api/users';
6
7
  import type { Task } from '@tuturuuu/types/primitives/Task';
7
8
  import { useSearchParams } from 'next/navigation';
8
9
  import { useCallback, useEffect, useRef, useState } from 'react';
@@ -12,6 +13,10 @@ import {
12
13
  WorkspacePresenceProvider,
13
14
  } from '../providers/workspace-presence-provider';
14
15
  import { dispatchRecentSidebarVisit } from './recent-sidebar-events';
16
+ import {
17
+ normalizeTaskDialogPresentation,
18
+ TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID,
19
+ } from './task-dialog-presentation';
15
20
  import { TaskEditDialog } from './task-edit-dialog';
16
21
  import {
17
22
  dispatchTaskOpenResult,
@@ -146,19 +151,23 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
146
151
  // Read draft mode preference from user config (same query key as useUserBooleanConfig)
147
152
  const { data: draftModeRaw } = useQuery({
148
153
  queryKey: ['user-config', 'TASK_DRAFT_MODE_ENABLED'],
149
- queryFn: async () => {
150
- const res = await fetch(
151
- '/api/v1/users/me/configs/TASK_DRAFT_MODE_ENABLED',
152
- { cache: 'no-store' }
153
- );
154
- if (!res.ok) return 'false';
155
- const data = await res.json();
156
- return (data.value as string) ?? 'false';
157
- },
154
+ queryFn: async () =>
155
+ (await getUserConfig('TASK_DRAFT_MODE_ENABLED')).value ?? 'false',
158
156
  staleTime: 5 * 60 * 1000,
159
157
  });
160
158
  const draftModeEnabled = draftModeRaw === 'true';
161
159
 
160
+ const { data: defaultPresentationRaw } = useQuery({
161
+ queryKey: ['user-config', TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID],
162
+ queryFn: async () =>
163
+ (await getUserConfig(TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID)).value ??
164
+ 'compact',
165
+ staleTime: 5 * 60 * 1000,
166
+ });
167
+ const defaultPresentation = normalizeTaskDialogPresentation(
168
+ defaultPresentationRaw
169
+ );
170
+
162
171
  const handleClose = () => {
163
172
  triggerClose();
164
173
  };
@@ -226,30 +235,12 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
226
235
 
227
236
  const openTaskFromCurrentWorkspace = useCallback(
228
237
  async (taskId: string) => {
229
- try {
230
- const { task } = await getWorkspaceTask(wsId, taskId, {
231
- fetch: (input, init) =>
232
- fetch(new URL(String(input), window.location.origin).toString(), {
233
- ...init,
234
- cache: 'no-store',
235
- }),
236
- });
237
-
238
- const boardId = task.board_id;
239
- if (!boardId) {
240
- return false;
241
- }
242
-
243
- openTask(task as Task, boardId, undefined, false, {
244
- taskWsId: wsId,
245
- taskWorkspacePersonal: isPersonalWorkspace,
246
- });
247
- return true;
248
- } catch {
249
- return false;
250
- }
238
+ return openTaskById(taskId, {
239
+ taskWsId: wsId,
240
+ taskWorkspacePersonal: isPersonalWorkspace,
241
+ });
251
242
  },
252
- [isPersonalWorkspace, openTask, wsId]
243
+ [isPersonalWorkspace, openTaskById, wsId]
253
244
  );
254
245
 
255
246
  useEffect(() => {
@@ -269,40 +260,12 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
269
260
  };
270
261
 
271
262
  void (async () => {
272
- if (requestedWsId) {
273
- try {
274
- const { task } = await getWorkspaceTask(requestedWsId, taskId, {
275
- fetch: (input, init) =>
276
- fetch(
277
- new URL(String(input), window.location.origin).toString(),
278
- {
279
- ...init,
280
- cache: 'no-store',
281
- }
282
- ),
283
- });
284
-
285
- const taskWithList = task as {
286
- board_id?: string | null;
287
- list?: {
288
- board_id?: string | null;
289
- } | null;
290
- };
291
- const boardId =
292
- taskWithList.board_id || taskWithList.list?.board_id;
293
- if (boardId) {
294
- openTask(task as Task, boardId, undefined, false, {
295
- taskWsId: requestedWsId,
296
- });
297
- emitOpenResult(true);
298
- return;
299
- }
300
- } catch {
301
- // Fall through to the generic current-user lookup below.
302
- }
303
- }
304
-
305
- const opened = await openTaskById(taskId);
263
+ const opened = await openTaskById(taskId, {
264
+ taskWsId: requestedWsId,
265
+ taskWorkspacePersonal: requestedWsId
266
+ ? undefined
267
+ : isPersonalWorkspace,
268
+ });
306
269
  emitOpenResult(opened);
307
270
  })();
308
271
  };
@@ -318,7 +281,7 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
318
281
  handleTaskOpenRequest as EventListener
319
282
  );
320
283
  };
321
- }, [openTaskById, openTask]);
284
+ }, [isPersonalWorkspace, openTaskById]);
322
285
 
323
286
  useEffect(() => {
324
287
  const canonicalTaskId = searchParams.get('task');
@@ -348,7 +311,10 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
348
311
  return;
349
312
  }
350
313
 
351
- void openTaskById(legacyTaskId);
314
+ void openTaskById(legacyTaskId, {
315
+ taskWsId: wsId,
316
+ taskWorkspacePersonal: isPersonalWorkspace,
317
+ });
352
318
  const nextSearchParams = new URLSearchParams(searchParams.toString());
353
319
  nextSearchParams.delete('openTaskId');
354
320
 
@@ -358,7 +324,7 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
358
324
  : window.location.pathname;
359
325
 
360
326
  window.history.replaceState(window.history.state, '', nextUrl);
361
- }, [openTaskById, searchParams]);
327
+ }, [isPersonalWorkspace, openTaskById, searchParams, wsId]);
362
328
 
363
329
  // Open subtask creation dialog for the current task
364
330
  const handleAddSubtask = useCallback(() => {
@@ -411,6 +377,31 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
411
377
  const handleAddBlockedByTask = () => handleAddRelationship('blocked-by');
412
378
  const handleAddRelatedTask = () => handleAddRelationship('related');
413
379
 
380
+ const handleRetryTaskLoad = useCallback(() => {
381
+ if (!state.task?.id) return;
382
+
383
+ void openTaskById(state.task.id, {
384
+ initialTask: state.task,
385
+ boardId: state.boardId,
386
+ availableLists: state.availableLists,
387
+ fakeTaskUrl: state.fakeTaskUrl,
388
+ taskWsId: state.taskWsId,
389
+ taskWorkspacePersonal: state.taskWorkspacePersonal,
390
+ taskWorkspaceTier: state.taskWorkspaceTier,
391
+ initialSharedContext: state.initialSharedContext,
392
+ });
393
+ }, [
394
+ openTaskById,
395
+ state.initialSharedContext,
396
+ state.availableLists,
397
+ state.boardId,
398
+ state.fakeTaskUrl,
399
+ state.task,
400
+ state.taskWorkspacePersonal,
401
+ state.taskWorkspaceTier,
402
+ state.taskWsId,
403
+ ]);
404
+
414
405
  // Track presence location when the dialog is open in edit mode.
415
406
  // On kanban boards, BoardUserPresenceAvatarsComponent also calls updateLocation
416
407
  // with the same args — this is idempotent (same location = no-op).
@@ -419,7 +410,14 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
419
410
  const wsUpdateLocation = wsPresence?.updateLocation;
420
411
 
421
412
  useEffect(() => {
422
- if (!wsUpdateLocation || !state.isOpen || state.mode === 'create') return;
413
+ if (
414
+ !wsUpdateLocation ||
415
+ !state.isOpen ||
416
+ state.mode === 'create' ||
417
+ state.isHydratingTask ||
418
+ state.taskLoadError
419
+ )
420
+ return;
423
421
  const taskId = state.task?.id;
424
422
  const boardId = state.boardId;
425
423
  if (!taskId || !boardId) return;
@@ -433,6 +431,8 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
433
431
  wsUpdateLocation,
434
432
  state.isOpen,
435
433
  state.mode,
434
+ state.isHydratingTask,
435
+ state.taskLoadError,
436
436
  state.task?.id,
437
437
  state.boardId,
438
438
  ]);
@@ -441,7 +441,9 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
441
441
  if (
442
442
  typeof window === 'undefined' ||
443
443
  !state.isOpen ||
444
- state.mode === 'create'
444
+ state.mode === 'create' ||
445
+ state.isHydratingTask ||
446
+ state.taskLoadError
445
447
  ) {
446
448
  return;
447
449
  }
@@ -493,19 +495,24 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
493
495
  }, [
494
496
  state.boardId,
495
497
  state.isOpen,
498
+ state.isHydratingTask,
496
499
  state.mode,
497
500
  state.task,
498
501
  state.taskWsId,
499
502
  state.taskWorkspacePersonal,
503
+ state.taskLoadError,
500
504
  isPersonalWorkspace,
501
505
  wsId,
502
506
  ]);
503
507
 
504
- // Determine if the task needs its own presence provider (cross-workspace tasks)
508
+ // Determine if the task needs its own presence provider (cross-workspace tasks).
509
+ // Keep the provider shell mounted from the initial snapshot when taskWsId is
510
+ // already known, otherwise hydration can wrap the open dialog in a new
511
+ // provider and Radix replays the compact dialog entrance animation.
505
512
  const needsOwnProvider =
506
- state.realtimeEnabled &&
507
- state.taskWsId &&
508
- (!wsPresence?.realtimeEnabled || state.taskWsId !== wsId);
513
+ state.taskWsId && (!wsPresence?.realtimeEnabled || state.taskWsId !== wsId);
514
+ const ownProviderEnabled =
515
+ !!state.realtimeEnabled && !state.taskWorkspacePersonal;
509
516
 
510
517
  if (!state.isOpen || !state.task) {
511
518
  return null;
@@ -519,16 +526,25 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
519
526
  boardId={state.boardId || ''}
520
527
  isOpen={state.isOpen}
521
528
  availableLists={state.availableLists}
529
+ sharedContext={
530
+ state.isHydratingTask || state.taskLoadError
531
+ ? state.initialSharedContext
532
+ : undefined
533
+ }
522
534
  filters={state.filters}
523
535
  mode={state.mode}
524
536
  collaborationMode={state.collaborationMode}
525
537
  realtimeEnabled={state.realtimeEnabled}
538
+ isHydratingTask={state.isHydratingTask}
539
+ taskLoadError={state.taskLoadError}
540
+ taskHydrationVersion={state.taskHydrationVersion}
526
541
  isPersonalWorkspace={isPersonalWorkspace}
527
542
  parentTaskId={state.parentTaskId}
528
543
  parentTaskName={state.parentTaskName}
529
544
  pendingRelationship={state.pendingRelationship}
530
545
  currentUser={currentUser || undefined}
531
546
  draftModeEnabled={draftModeEnabled}
547
+ defaultPresentation={defaultPresentation}
532
548
  draftId={state.draftId}
533
549
  onClose={handleClose}
534
550
  onUpdate={triggerUpdate}
@@ -538,6 +554,7 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
538
554
  onAddBlockingTask={handleAddBlockingTask}
539
555
  onAddBlockedByTask={handleAddBlockedByTask}
540
556
  onAddRelatedTask={handleAddRelatedTask}
557
+ onRetryTaskLoad={handleRetryTaskLoad}
541
558
  />
542
559
  );
543
560
 
@@ -546,7 +563,7 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
546
563
  <WorkspacePresenceProvider
547
564
  wsId={state.taskWsId}
548
565
  tier={state.taskWorkspaceTier ?? null}
549
- enabled={!state.taskWorkspacePersonal}
566
+ enabled={ownProviderEnabled}
550
567
  >
551
568
  {dialog}
552
569
  </WorkspacePresenceProvider>
@@ -0,0 +1,11 @@
1
+ export const TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID =
2
+ 'TASK_DIALOG_DEFAULT_PRESENTATION';
3
+
4
+ export type TaskDialogPresentation = 'compact' | 'fullscreen';
5
+
6
+ export function normalizeTaskDialogPresentation(
7
+ value: unknown,
8
+ fallback: TaskDialogPresentation = 'compact'
9
+ ): TaskDialogPresentation {
10
+ return value === 'fullscreen' || value === 'compact' ? value : fallback;
11
+ }