@tuturuuu/ui 0.5.0 → 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 +29 -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 +50 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +21 -1
- 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/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +73 -26
- 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 +2 -1
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +4 -4
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +298 -34
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +219 -46
- 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 +1 -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 +64 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +42 -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 +111 -37
- package/src/components/ui/finance/wallets/wallets-page.tsx +38 -78
- 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/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/use-my-tasks-state.ts +2 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +114 -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 +128 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +104 -69
- 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-name-input.test.tsx +17 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +151 -111
- 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/task-dialog-actions.tsx +5 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +584 -53
- package/src/hooks/useBoardRealtime.ts +54 -1
- package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
- package/src/hooks/useTaskUserRealtime.ts +338 -0
|
@@ -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) => {
|
|
@@ -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={
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|