@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.
- package/CHANGELOG.md +43 -0
- package/package.json +41 -34
- package/src/components/ui/currency-input.tsx +65 -23
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
- package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
- package/src/components/ui/custom/combobox.test.tsx +141 -0
- package/src/components/ui/custom/combobox.tsx +105 -36
- package/src/components/ui/custom/settings/task-settings.tsx +126 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
- package/src/components/ui/custom/sidebar-context.tsx +68 -6
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
- package/src/components/ui/finance/finance-layout.tsx +2 -4
- package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
- package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
- package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
- package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
- package/src/components/ui/finance/transactions/form-types.ts +23 -0
- package/src/components/ui/finance/transactions/form.tsx +81 -22
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
- package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
- package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
- package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
- package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
- package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
- package/src/components/ui/finance/wallets/columns.test.ts +56 -0
- package/src/components/ui/finance/wallets/columns.tsx +196 -43
- package/src/components/ui/finance/wallets/form.test.tsx +79 -14
- package/src/components/ui/finance/wallets/form.tsx +41 -197
- package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
- package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
- package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
- package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
- package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
- package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
- package/src/components/ui/storefront/accent-button.tsx +33 -0
- package/src/components/ui/storefront/cart-summary.tsx +140 -0
- package/src/components/ui/storefront/empty-listings.tsx +32 -0
- package/src/components/ui/storefront/hero-panel.tsx +70 -0
- package/src/components/ui/storefront/image-panel.tsx +40 -0
- package/src/components/ui/storefront/index.ts +12 -0
- package/src/components/ui/storefront/listing-card.tsx +129 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
- package/src/components/ui/storefront/storefront-surface.tsx +235 -0
- package/src/components/ui/storefront/types.ts +99 -0
- package/src/components/ui/storefront/utils.ts +90 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
- package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
- package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
- package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
- package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
- package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
- package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
- package/src/hooks/use-task-actions.ts +45 -0
- package/src/hooks/useBoardRealtime.ts +54 -1
- package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
- package/src/hooks/useTaskUserRealtime.ts +338 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
useRef,
|
|
15
15
|
useState,
|
|
16
16
|
} from 'react';
|
|
17
|
+
import type { SharedTaskContext } from '../shared/task-edit-dialog/hooks/use-task-data';
|
|
17
18
|
import type {
|
|
18
19
|
PendingRelationship,
|
|
19
20
|
PendingRelationshipType,
|
|
@@ -58,6 +59,26 @@ interface TaskDialogState {
|
|
|
58
59
|
taskWorkspacePersonal?: boolean;
|
|
59
60
|
/** The task workspace tier used to gate cursor tracking for edit mode */
|
|
60
61
|
taskWorkspaceTier?: WorkspaceProductTier;
|
|
62
|
+
/** Initial board/list context used for immediate partial-task rendering. */
|
|
63
|
+
initialSharedContext?: SharedTaskContext;
|
|
64
|
+
/** True while an existing task was opened from a partial snapshot and is hydrating. */
|
|
65
|
+
isHydratingTask?: boolean;
|
|
66
|
+
/** True when the latest hydration request failed after the dialog already opened. */
|
|
67
|
+
taskLoadError?: boolean;
|
|
68
|
+
/** Bumps when async task hydration replaces a partial snapshot. */
|
|
69
|
+
taskHydrationVersion?: number;
|
|
70
|
+
taskOpenRequestId?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface OpenTaskByIdOptions {
|
|
74
|
+
initialTask?: Partial<Task>;
|
|
75
|
+
boardId?: string;
|
|
76
|
+
availableLists?: TaskList[];
|
|
77
|
+
fakeTaskUrl?: boolean;
|
|
78
|
+
taskWsId?: string;
|
|
79
|
+
taskWorkspacePersonal?: boolean;
|
|
80
|
+
taskWorkspaceTier?: WorkspaceProductTier;
|
|
81
|
+
initialSharedContext?: SharedTaskContext;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
interface TaskDialogContextValue {
|
|
@@ -85,7 +106,10 @@ interface TaskDialogContextValue {
|
|
|
85
106
|
) => void;
|
|
86
107
|
|
|
87
108
|
// Open task by ID (fetches task data first)
|
|
88
|
-
openTaskById: (
|
|
109
|
+
openTaskById: (
|
|
110
|
+
taskId: string,
|
|
111
|
+
options?: OpenTaskByIdOptions
|
|
112
|
+
) => Promise<boolean>;
|
|
89
113
|
|
|
90
114
|
// Open dialog for creating new task
|
|
91
115
|
createTask: (
|
|
@@ -178,9 +202,11 @@ export function TaskDialogProvider({
|
|
|
178
202
|
const [state, setState] = useState<TaskDialogState>({
|
|
179
203
|
isOpen: false,
|
|
180
204
|
});
|
|
205
|
+
const stateRef = useRef(state);
|
|
181
206
|
const isDialogOpenRef = useRef(state.isOpen);
|
|
182
207
|
const queuedDialogStatesRef = useRef<TaskDialogState[]>([]);
|
|
183
208
|
const queuedOpenTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
209
|
+
const taskOpenRequestIdRef = useRef(0);
|
|
184
210
|
const closeRequestHandlerRef = useRef<
|
|
185
211
|
(() => boolean | undefined | Promise<boolean | undefined>) | null
|
|
186
212
|
>(null);
|
|
@@ -283,7 +309,52 @@ export function TaskDialogProvider({
|
|
|
283
309
|
[closeDialog]
|
|
284
310
|
);
|
|
285
311
|
|
|
312
|
+
const replaceHydratingDialogState = useCallback(
|
|
313
|
+
(requestId: number, nextDialogState: TaskDialogState) => {
|
|
314
|
+
queuedDialogStatesRef.current = queuedDialogStatesRef.current.map(
|
|
315
|
+
(queuedDialogState) =>
|
|
316
|
+
queuedDialogState.taskOpenRequestId === requestId
|
|
317
|
+
? nextDialogState
|
|
318
|
+
: queuedDialogState
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
setState((currentState) =>
|
|
322
|
+
currentState.taskOpenRequestId === requestId
|
|
323
|
+
? nextDialogState
|
|
324
|
+
: currentState
|
|
325
|
+
);
|
|
326
|
+
},
|
|
327
|
+
[]
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const markHydratingDialogFailed = useCallback((requestId: number) => {
|
|
331
|
+
queuedDialogStatesRef.current = queuedDialogStatesRef.current.map(
|
|
332
|
+
(queuedDialogState) =>
|
|
333
|
+
queuedDialogState.taskOpenRequestId === requestId
|
|
334
|
+
? {
|
|
335
|
+
...queuedDialogState,
|
|
336
|
+
isHydratingTask: false,
|
|
337
|
+
taskLoadError: true,
|
|
338
|
+
taskHydrationVersion:
|
|
339
|
+
(queuedDialogState.taskHydrationVersion ?? 0) + 1,
|
|
340
|
+
}
|
|
341
|
+
: queuedDialogState
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
setState((currentState) =>
|
|
345
|
+
currentState.taskOpenRequestId === requestId
|
|
346
|
+
? {
|
|
347
|
+
...currentState,
|
|
348
|
+
isHydratingTask: false,
|
|
349
|
+
taskLoadError: true,
|
|
350
|
+
taskHydrationVersion: (currentState.taskHydrationVersion ?? 0) + 1,
|
|
351
|
+
}
|
|
352
|
+
: currentState
|
|
353
|
+
);
|
|
354
|
+
}, []);
|
|
355
|
+
|
|
286
356
|
useEffect(() => {
|
|
357
|
+
stateRef.current = state;
|
|
287
358
|
isDialogOpenRef.current = state.isOpen;
|
|
288
359
|
|
|
289
360
|
if (state.isOpen) {
|
|
@@ -292,7 +363,7 @@ export function TaskDialogProvider({
|
|
|
292
363
|
|
|
293
364
|
closeRequestInFlightRef.current = false;
|
|
294
365
|
flushQueuedDialogState();
|
|
295
|
-
}, [flushQueuedDialogState, state
|
|
366
|
+
}, [flushQueuedDialogState, state]);
|
|
296
367
|
|
|
297
368
|
useEffect(() => {
|
|
298
369
|
return () => {
|
|
@@ -344,48 +415,108 @@ export function TaskDialogProvider({
|
|
|
344
415
|
);
|
|
345
416
|
|
|
346
417
|
const openTaskById = useCallback(
|
|
347
|
-
async (taskId: string) => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
418
|
+
async (taskId: string, options?: OpenTaskByIdOptions) => {
|
|
419
|
+
const requestId = taskOpenRequestIdRef.current + 1;
|
|
420
|
+
taskOpenRequestIdRef.current = requestId;
|
|
421
|
+
|
|
422
|
+
const initialTaskSnapshot = options?.initialTask;
|
|
423
|
+
const now = new Date().toISOString();
|
|
424
|
+
const initialTask = {
|
|
425
|
+
name: '',
|
|
426
|
+
description: '',
|
|
427
|
+
list_id: '',
|
|
428
|
+
display_number: 0,
|
|
429
|
+
created_at: now,
|
|
430
|
+
updated_at: now,
|
|
431
|
+
deleted: false,
|
|
432
|
+
archived: false,
|
|
433
|
+
labels: [],
|
|
434
|
+
assignees: [],
|
|
435
|
+
projects: [],
|
|
436
|
+
...initialTaskSnapshot,
|
|
437
|
+
id: taskId,
|
|
438
|
+
} as Task;
|
|
439
|
+
const initialTaskWithBoard = initialTaskSnapshot as
|
|
440
|
+
| (Partial<Task> & {
|
|
441
|
+
board_id?: string | null;
|
|
442
|
+
list?: { board_id?: string | null } | null;
|
|
443
|
+
})
|
|
444
|
+
| undefined;
|
|
445
|
+
const initialBoardId =
|
|
446
|
+
options?.boardId ??
|
|
447
|
+
initialTaskWithBoard?.board_id ??
|
|
448
|
+
initialTaskWithBoard?.list?.board_id ??
|
|
449
|
+
undefined;
|
|
450
|
+
const initialTaskWorkspacePersonal =
|
|
451
|
+
options?.taskWorkspacePersonal ?? isPersonalWorkspace;
|
|
362
452
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
453
|
+
const initialDialogState: TaskDialogState = {
|
|
454
|
+
isOpen: true,
|
|
455
|
+
task: initialTask,
|
|
456
|
+
boardId: initialBoardId,
|
|
457
|
+
mode: 'edit',
|
|
458
|
+
availableLists: options?.availableLists,
|
|
459
|
+
collaborationMode: false,
|
|
460
|
+
realtimeEnabled: false,
|
|
461
|
+
fakeTaskUrl: options?.fakeTaskUrl,
|
|
462
|
+
taskWsId: options?.taskWsId,
|
|
463
|
+
taskWorkspacePersonal: initialTaskWorkspacePersonal,
|
|
464
|
+
taskWorkspaceTier: options?.taskWorkspaceTier,
|
|
465
|
+
initialSharedContext: options?.initialSharedContext,
|
|
466
|
+
isHydratingTask: true,
|
|
467
|
+
taskLoadError: false,
|
|
468
|
+
taskHydrationVersion: 0,
|
|
469
|
+
taskOpenRequestId: requestId,
|
|
470
|
+
};
|
|
471
|
+
const currentState = stateRef.current;
|
|
472
|
+
|
|
473
|
+
if (
|
|
474
|
+
currentState.isOpen &&
|
|
475
|
+
currentState.task?.id === taskId &&
|
|
476
|
+
currentState.taskLoadError
|
|
477
|
+
) {
|
|
478
|
+
setState(initialDialogState);
|
|
479
|
+
} else {
|
|
480
|
+
queueDialogState(initialDialogState);
|
|
481
|
+
}
|
|
367
482
|
|
|
368
|
-
|
|
483
|
+
try {
|
|
484
|
+
const { getTaskDialogHydration } = await import(
|
|
485
|
+
'@tuturuuu/internal-api/tasks'
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
const response = await getTaskDialogHydration(
|
|
489
|
+
taskId,
|
|
490
|
+
{
|
|
491
|
+
taskWsId: options?.taskWsId,
|
|
492
|
+
taskWorkspacePersonal: options?.taskWorkspacePersonal,
|
|
493
|
+
taskWorkspaceTier: options?.taskWorkspaceTier,
|
|
494
|
+
},
|
|
495
|
+
{
|
|
369
496
|
fetch: (input, init) =>
|
|
370
497
|
fetch(new URL(String(input), window.location.origin).toString(), {
|
|
371
498
|
...init,
|
|
372
499
|
cache: 'no-store',
|
|
373
500
|
}),
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
501
|
+
}
|
|
502
|
+
);
|
|
378
503
|
|
|
379
504
|
if (!response) {
|
|
505
|
+
markHydratingDialogFailed(requestId);
|
|
380
506
|
return false;
|
|
381
507
|
}
|
|
382
508
|
|
|
383
|
-
const transformedTask = response.task
|
|
509
|
+
const transformedTask = response.task as Task & {
|
|
510
|
+
board_id?: string | null;
|
|
511
|
+
list?: {
|
|
512
|
+
board_id?: string | null;
|
|
513
|
+
} | null;
|
|
514
|
+
};
|
|
384
515
|
const taskWsId = response.taskWsId;
|
|
385
516
|
const taskWorkspacePersonal = response.taskWorkspacePersonal;
|
|
386
517
|
const taskWorkspaceTier = response.taskWorkspaceTier;
|
|
387
518
|
const isTaskWorkspacePersonal =
|
|
388
|
-
taskWorkspacePersonal ??
|
|
519
|
+
taskWorkspacePersonal ?? initialTaskWorkspacePersonal;
|
|
389
520
|
|
|
390
521
|
// Realtime sync (auto-save via Yjs) is always enabled in edit mode.
|
|
391
522
|
// Cursor presence requires tier check and non-personal workspace.
|
|
@@ -394,26 +525,40 @@ export function TaskDialogProvider({
|
|
|
394
525
|
taskWorkspaceTier
|
|
395
526
|
);
|
|
396
527
|
|
|
397
|
-
|
|
398
|
-
queueDialogState({
|
|
528
|
+
replaceHydratingDialogState(requestId, {
|
|
399
529
|
isOpen: true,
|
|
400
530
|
task: transformedTask as Task,
|
|
401
|
-
boardId:
|
|
531
|
+
boardId:
|
|
532
|
+
transformedTask.board_id ??
|
|
533
|
+
transformedTask.list?.board_id ??
|
|
534
|
+
initialBoardId,
|
|
402
535
|
mode: 'edit',
|
|
403
|
-
availableLists:
|
|
536
|
+
availableLists:
|
|
537
|
+
response.availableLists || options?.availableLists || undefined,
|
|
404
538
|
collaborationMode: shouldEnableCursors,
|
|
405
539
|
realtimeEnabled: true,
|
|
540
|
+
fakeTaskUrl: options?.fakeTaskUrl,
|
|
406
541
|
taskWsId,
|
|
407
542
|
taskWorkspacePersonal: isTaskWorkspacePersonal,
|
|
408
543
|
taskWorkspaceTier,
|
|
544
|
+
isHydratingTask: false,
|
|
545
|
+
taskLoadError: false,
|
|
546
|
+
taskHydrationVersion: 1,
|
|
547
|
+
taskOpenRequestId: requestId,
|
|
409
548
|
});
|
|
410
549
|
return true;
|
|
411
|
-
} catch
|
|
412
|
-
|
|
550
|
+
} catch {
|
|
551
|
+
markHydratingDialogFailed(requestId);
|
|
413
552
|
return false;
|
|
414
553
|
}
|
|
415
554
|
},
|
|
416
|
-
[
|
|
555
|
+
[
|
|
556
|
+
canUseTaskCursors,
|
|
557
|
+
isPersonalWorkspace,
|
|
558
|
+
markHydratingDialogFailed,
|
|
559
|
+
queueDialogState,
|
|
560
|
+
replaceHydratingDialogState,
|
|
561
|
+
]
|
|
417
562
|
);
|
|
418
563
|
|
|
419
564
|
const createTask = useCallback(
|
|
@@ -13,8 +13,12 @@ import { RECENT_SIDEBAR_VISIT_EVENT } from '../recent-sidebar-events';
|
|
|
13
13
|
import { TaskDialogManager } from '../task-dialog-manager';
|
|
14
14
|
import { REQUEST_OPEN_TASK_EVENT } from '../task-open-events';
|
|
15
15
|
|
|
16
|
-
const { mockSearchParams } = vi.hoisted(() => ({
|
|
16
|
+
const { mockSearchParams, taskDialogRenderStats } = vi.hoisted(() => ({
|
|
17
17
|
mockSearchParams: new URLSearchParams(),
|
|
18
|
+
taskDialogRenderStats: {
|
|
19
|
+
mounts: 0,
|
|
20
|
+
unmounts: 0,
|
|
21
|
+
},
|
|
18
22
|
}));
|
|
19
23
|
|
|
20
24
|
// Mock Next.js navigation (no longer needs useRouter/usePathname for URL manipulation)
|
|
@@ -34,7 +38,8 @@ vi.mock('next/navigation', () => ({
|
|
|
34
38
|
|
|
35
39
|
const {
|
|
36
40
|
mockGetCurrentUserProfile,
|
|
37
|
-
|
|
41
|
+
mockGetTaskDialogHydration,
|
|
42
|
+
mockGetUserConfig,
|
|
38
43
|
mockGetWorkspaceTask,
|
|
39
44
|
mockListWorkspaceLabels,
|
|
40
45
|
mockListWorkspaceMembers,
|
|
@@ -42,7 +47,8 @@ const {
|
|
|
42
47
|
mockResolveTaskProjectWorkspaceId,
|
|
43
48
|
} = vi.hoisted(() => ({
|
|
44
49
|
mockGetCurrentUserProfile: vi.fn(),
|
|
45
|
-
|
|
50
|
+
mockGetTaskDialogHydration: vi.fn(),
|
|
51
|
+
mockGetUserConfig: vi.fn(),
|
|
46
52
|
mockGetWorkspaceTask: vi.fn(),
|
|
47
53
|
mockListWorkspaceLabels: vi.fn(),
|
|
48
54
|
mockListWorkspaceMembers: vi.fn(),
|
|
@@ -55,13 +61,17 @@ vi.mock('@tuturuuu/internal-api', () => ({
|
|
|
55
61
|
}));
|
|
56
62
|
|
|
57
63
|
vi.mock('@tuturuuu/internal-api/tasks', () => ({
|
|
58
|
-
|
|
64
|
+
getTaskDialogHydration: mockGetTaskDialogHydration,
|
|
59
65
|
getWorkspaceTask: mockGetWorkspaceTask,
|
|
60
66
|
listWorkspaceLabels: mockListWorkspaceLabels,
|
|
61
67
|
listWorkspaceTaskProjectsByIds: mockListWorkspaceTaskProjectsByIds,
|
|
62
68
|
resolveTaskProjectWorkspaceId: mockResolveTaskProjectWorkspaceId,
|
|
63
69
|
}));
|
|
64
70
|
|
|
71
|
+
vi.mock('@tuturuuu/internal-api/users', () => ({
|
|
72
|
+
getUserConfig: mockGetUserConfig,
|
|
73
|
+
}));
|
|
74
|
+
|
|
65
75
|
vi.mock('@tuturuuu/internal-api/workspaces', () => ({
|
|
66
76
|
listWorkspaceMembers: mockListWorkspaceMembers,
|
|
67
77
|
}));
|
|
@@ -156,30 +166,52 @@ vi.mock('@tuturuuu/supabase/next/client', () => ({
|
|
|
156
166
|
// Mock the TaskEditDialog component since it's lazy-loaded
|
|
157
167
|
vi.mock('../task-edit-dialog', () => ({
|
|
158
168
|
TaskEditDialog: ({
|
|
169
|
+
defaultPresentation,
|
|
159
170
|
isOpen,
|
|
171
|
+
isHydratingTask,
|
|
172
|
+
taskLoadError,
|
|
160
173
|
task,
|
|
161
174
|
onClose,
|
|
162
175
|
onNavigateToTask,
|
|
163
176
|
}: {
|
|
177
|
+
defaultPresentation?: string;
|
|
164
178
|
isOpen: boolean;
|
|
179
|
+
isHydratingTask?: boolean;
|
|
180
|
+
taskLoadError?: boolean;
|
|
165
181
|
task?: Task;
|
|
166
182
|
onClose: () => void;
|
|
167
183
|
onNavigateToTask?: (taskId: string) => Promise<void>;
|
|
168
|
-
}) =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
184
|
+
}) => {
|
|
185
|
+
React.useEffect(() => {
|
|
186
|
+
taskDialogRenderStats.mounts += 1;
|
|
187
|
+
|
|
188
|
+
return () => {
|
|
189
|
+
taskDialogRenderStats.unmounts += 1;
|
|
190
|
+
};
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div
|
|
195
|
+
data-testid="task-edit-dialog"
|
|
196
|
+
data-default-presentation={defaultPresentation}
|
|
197
|
+
data-hydrating={String(!!isHydratingTask)}
|
|
198
|
+
data-load-error={String(!!taskLoadError)}
|
|
199
|
+
data-open={isOpen}
|
|
175
200
|
>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
201
|
+
{task && <div data-testid="task-name">{task.name}</div>}
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={() => void onNavigateToTask?.('task-2')}
|
|
205
|
+
data-testid="navigate-button"
|
|
206
|
+
>
|
|
207
|
+
Navigate
|
|
208
|
+
</button>
|
|
209
|
+
<button type="button" onClick={onClose} data-testid="close-button">
|
|
210
|
+
Close
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
},
|
|
183
215
|
}));
|
|
184
216
|
|
|
185
217
|
// Mock task data
|
|
@@ -218,6 +250,17 @@ function createTestQueryClient() {
|
|
|
218
250
|
});
|
|
219
251
|
}
|
|
220
252
|
|
|
253
|
+
function createDeferred<T>() {
|
|
254
|
+
let resolve!: (value: T) => void;
|
|
255
|
+
let reject!: (reason?: unknown) => void;
|
|
256
|
+
const promise = new Promise<T>((promiseResolve, promiseReject) => {
|
|
257
|
+
resolve = promiseResolve;
|
|
258
|
+
reject = promiseReject;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return { promise, resolve, reject };
|
|
262
|
+
}
|
|
263
|
+
|
|
221
264
|
function Wrapper({ children }: { children: React.ReactNode }) {
|
|
222
265
|
const queryClient = createTestQueryClient();
|
|
223
266
|
return (
|
|
@@ -236,6 +279,8 @@ beforeEach(() => {
|
|
|
236
279
|
mockSearchParams.forEach((_, key) => {
|
|
237
280
|
mockSearchParams.delete(key);
|
|
238
281
|
});
|
|
282
|
+
taskDialogRenderStats.mounts = 0;
|
|
283
|
+
taskDialogRenderStats.unmounts = 0;
|
|
239
284
|
pushStateSpy = vi.spyOn(window.history, 'pushState');
|
|
240
285
|
replaceStateSpy = vi.spyOn(window.history, 'replaceState');
|
|
241
286
|
mockGetCurrentUserProfile.mockResolvedValue({
|
|
@@ -244,7 +289,13 @@ beforeEach(() => {
|
|
|
244
289
|
email: 'user@example.com',
|
|
245
290
|
avatar_url: null,
|
|
246
291
|
});
|
|
247
|
-
|
|
292
|
+
mockGetUserConfig.mockImplementation((configId: string) =>
|
|
293
|
+
Promise.resolve({
|
|
294
|
+
value:
|
|
295
|
+
configId === 'TASK_DIALOG_DEFAULT_PRESENTATION' ? 'compact' : 'false',
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
mockGetTaskDialogHydration.mockResolvedValue({
|
|
248
299
|
task: {
|
|
249
300
|
...mockTask,
|
|
250
301
|
list: { board_id: 'board-1' },
|
|
@@ -348,6 +399,10 @@ describe('TaskDialogManager', () => {
|
|
|
348
399
|
'data-open',
|
|
349
400
|
'true'
|
|
350
401
|
);
|
|
402
|
+
expect(getByTestId('task-edit-dialog')).toHaveAttribute(
|
|
403
|
+
'data-default-presentation',
|
|
404
|
+
'compact'
|
|
405
|
+
);
|
|
351
406
|
expect(getByTestId('task-name')).toHaveTextContent('Test Task');
|
|
352
407
|
});
|
|
353
408
|
});
|
|
@@ -456,9 +511,9 @@ describe('TaskDialogManager', () => {
|
|
|
456
511
|
);
|
|
457
512
|
|
|
458
513
|
await waitFor(() => {
|
|
459
|
-
expect(
|
|
460
|
-
'workspace-1',
|
|
514
|
+
expect(mockGetTaskDialogHydration).toHaveBeenCalledWith(
|
|
461
515
|
'task-2',
|
|
516
|
+
expect.any(Object),
|
|
462
517
|
expect.any(Object)
|
|
463
518
|
);
|
|
464
519
|
});
|
|
@@ -468,7 +523,7 @@ describe('TaskDialogManager', () => {
|
|
|
468
523
|
'data-open',
|
|
469
524
|
'true'
|
|
470
525
|
);
|
|
471
|
-
expect(getByTestId('task-name')).toHaveTextContent('
|
|
526
|
+
expect(getByTestId('task-name')).toHaveTextContent('Test Task');
|
|
472
527
|
});
|
|
473
528
|
});
|
|
474
529
|
|
|
@@ -561,6 +616,147 @@ describe('TaskDialogManager', () => {
|
|
|
561
616
|
});
|
|
562
617
|
});
|
|
563
618
|
|
|
619
|
+
it('renders a hydrating task dialog immediately for shared task-open events', async () => {
|
|
620
|
+
const deferred = createDeferred<{
|
|
621
|
+
task: Task & { list?: { board_id?: string | null } | null };
|
|
622
|
+
availableLists: TaskList[];
|
|
623
|
+
taskWsId: string;
|
|
624
|
+
taskWorkspacePersonal: boolean;
|
|
625
|
+
taskWorkspaceTier: 'PRO';
|
|
626
|
+
}>();
|
|
627
|
+
mockGetTaskDialogHydration.mockReturnValueOnce(deferred.promise);
|
|
628
|
+
|
|
629
|
+
const { getByTestId } = render(
|
|
630
|
+
<Wrapper>
|
|
631
|
+
<TaskDialogManager wsId="workspace-1" />
|
|
632
|
+
</Wrapper>
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
act(() => {
|
|
636
|
+
window.dispatchEvent(
|
|
637
|
+
new CustomEvent(REQUEST_OPEN_TASK_EVENT, {
|
|
638
|
+
detail: { taskId: 'task-42' },
|
|
639
|
+
})
|
|
640
|
+
);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
expect(getByTestId('task-edit-dialog')).toHaveAttribute(
|
|
644
|
+
'data-hydrating',
|
|
645
|
+
'true'
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
await act(async () => {
|
|
649
|
+
deferred.resolve({
|
|
650
|
+
task: {
|
|
651
|
+
...mockTask,
|
|
652
|
+
id: 'task-42',
|
|
653
|
+
name: 'Hydrated Event Task',
|
|
654
|
+
list: { board_id: 'board-1' },
|
|
655
|
+
},
|
|
656
|
+
availableLists: [mockList],
|
|
657
|
+
taskWsId: 'workspace-1',
|
|
658
|
+
taskWorkspacePersonal: false,
|
|
659
|
+
taskWorkspaceTier: 'PRO',
|
|
660
|
+
});
|
|
661
|
+
await Promise.resolve();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
await waitFor(() => {
|
|
665
|
+
expect(getByTestId('task-edit-dialog')).toHaveAttribute(
|
|
666
|
+
'data-hydrating',
|
|
667
|
+
'false'
|
|
668
|
+
);
|
|
669
|
+
expect(getByTestId('task-name')).toHaveTextContent('Hydrated Event Task');
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('keeps the same dialog mounted when a source-workspace task finishes hydrating', async () => {
|
|
674
|
+
const deferred = createDeferred<{
|
|
675
|
+
task: Task & { list?: { board_id?: string | null } | null };
|
|
676
|
+
availableLists: TaskList[];
|
|
677
|
+
taskWsId: string;
|
|
678
|
+
taskWorkspacePersonal: boolean;
|
|
679
|
+
taskWorkspaceTier: 'PRO';
|
|
680
|
+
}>();
|
|
681
|
+
mockGetTaskDialogHydration.mockReturnValueOnce(deferred.promise);
|
|
682
|
+
|
|
683
|
+
const TestComponent = () => {
|
|
684
|
+
const { openTaskById } = useTaskDialogContext();
|
|
685
|
+
|
|
686
|
+
React.useEffect(() => {
|
|
687
|
+
void openTaskById('external-task-1', {
|
|
688
|
+
initialTask: {
|
|
689
|
+
...mockTask,
|
|
690
|
+
id: 'external-task-1',
|
|
691
|
+
name: 'External snapshot',
|
|
692
|
+
},
|
|
693
|
+
boardId: 'external-board-1',
|
|
694
|
+
availableLists: [
|
|
695
|
+
{
|
|
696
|
+
...mockList,
|
|
697
|
+
id: 'external-list-1',
|
|
698
|
+
board_id: 'external-board-1',
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
taskWsId: 'source-workspace-1',
|
|
702
|
+
taskWorkspacePersonal: false,
|
|
703
|
+
});
|
|
704
|
+
}, [openTaskById]);
|
|
705
|
+
|
|
706
|
+
return <TaskDialogManager wsId="personal-workspace-1" />;
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const { getByTestId } = render(
|
|
710
|
+
<Wrapper>
|
|
711
|
+
<TestComponent />
|
|
712
|
+
</Wrapper>
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
await waitFor(() => {
|
|
716
|
+
expect(getByTestId('task-edit-dialog')).toHaveAttribute(
|
|
717
|
+
'data-hydrating',
|
|
718
|
+
'true'
|
|
719
|
+
);
|
|
720
|
+
expect(getByTestId('task-name')).toHaveTextContent('External snapshot');
|
|
721
|
+
});
|
|
722
|
+
expect(taskDialogRenderStats).toMatchObject({
|
|
723
|
+
mounts: 1,
|
|
724
|
+
unmounts: 0,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
await act(async () => {
|
|
728
|
+
deferred.resolve({
|
|
729
|
+
task: {
|
|
730
|
+
...mockTask,
|
|
731
|
+
id: 'external-task-1',
|
|
732
|
+
name: 'Hydrated external task',
|
|
733
|
+
list: { board_id: 'external-board-1' },
|
|
734
|
+
},
|
|
735
|
+
availableLists: [
|
|
736
|
+
{ ...mockList, id: 'external-list-1', board_id: 'external-board-1' },
|
|
737
|
+
],
|
|
738
|
+
taskWsId: 'source-workspace-1',
|
|
739
|
+
taskWorkspacePersonal: false,
|
|
740
|
+
taskWorkspaceTier: 'PRO',
|
|
741
|
+
});
|
|
742
|
+
await Promise.resolve();
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
await waitFor(() => {
|
|
746
|
+
expect(getByTestId('task-edit-dialog')).toHaveAttribute(
|
|
747
|
+
'data-hydrating',
|
|
748
|
+
'false'
|
|
749
|
+
);
|
|
750
|
+
expect(getByTestId('task-name')).toHaveTextContent(
|
|
751
|
+
'Hydrated external task'
|
|
752
|
+
);
|
|
753
|
+
});
|
|
754
|
+
expect(taskDialogRenderStats).toMatchObject({
|
|
755
|
+
mounts: 1,
|
|
756
|
+
unmounts: 0,
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
564
760
|
it('opens a task from the canonical task query parameter', async () => {
|
|
565
761
|
mockSearchParams.set('task', 'task-42');
|
|
566
762
|
|
|
@@ -571,9 +767,9 @@ describe('TaskDialogManager', () => {
|
|
|
571
767
|
);
|
|
572
768
|
|
|
573
769
|
await waitFor(() => {
|
|
574
|
-
expect(
|
|
575
|
-
'workspace-1',
|
|
770
|
+
expect(mockGetTaskDialogHydration).toHaveBeenCalledWith(
|
|
576
771
|
'task-42',
|
|
772
|
+
expect.any(Object),
|
|
577
773
|
expect.any(Object)
|
|
578
774
|
);
|
|
579
775
|
});
|
|
@@ -616,7 +812,7 @@ describe('TaskDialogManager', () => {
|
|
|
616
812
|
expect(getByTestId('task-name')).toHaveTextContent('Test Task');
|
|
617
813
|
});
|
|
618
814
|
|
|
619
|
-
|
|
815
|
+
mockGetTaskDialogHydration.mockClear();
|
|
620
816
|
mockGetWorkspaceTask.mockClear();
|
|
621
817
|
|
|
622
818
|
fireEvent.click(getByTestId('navigate-button'));
|
|
@@ -625,7 +821,7 @@ describe('TaskDialogManager', () => {
|
|
|
625
821
|
expect(getByTestId('task-name')).toHaveTextContent('Cached Related Task');
|
|
626
822
|
});
|
|
627
823
|
|
|
628
|
-
expect(
|
|
824
|
+
expect(mockGetTaskDialogHydration).not.toHaveBeenCalled();
|
|
629
825
|
});
|
|
630
826
|
|
|
631
827
|
it('should use replaceState to revert URL when dialog closes', async () => {
|
|
@@ -142,9 +142,7 @@ export function BoardClient({
|
|
|
142
142
|
// Fetch workspace labels once at the board level
|
|
143
143
|
const { data: workspaceLabels = [] } = useWorkspaceLabels(boardWorkspaceId);
|
|
144
144
|
|
|
145
|
-
const { broadcast } = useBoardRealtime(boardId
|
|
146
|
-
enabled: !workspace.personal,
|
|
147
|
-
});
|
|
145
|
+
const { broadcast } = useBoardRealtime(boardId);
|
|
148
146
|
|
|
149
147
|
const refreshActiveBoard = useCallback(
|
|
150
148
|
(options?: BoardRefreshOptions) => {
|