@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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
|
+
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
getTaskCardHydratingOpenOptions,
|
|
6
|
+
isExternalTaskSnapshot,
|
|
7
|
+
} from './task-card-open-options';
|
|
8
|
+
|
|
9
|
+
const task = {
|
|
10
|
+
id: 'task-1',
|
|
11
|
+
name: 'Visible task',
|
|
12
|
+
description: '',
|
|
13
|
+
list_id: 'list-1',
|
|
14
|
+
display_number: 7,
|
|
15
|
+
created_at: '2026-06-12T00:00:00.000Z',
|
|
16
|
+
end_date: null,
|
|
17
|
+
priority: 'normal',
|
|
18
|
+
} satisfies Task;
|
|
19
|
+
|
|
20
|
+
const list = {
|
|
21
|
+
id: 'list-1',
|
|
22
|
+
name: 'To Do',
|
|
23
|
+
board_id: 'board-1',
|
|
24
|
+
position: 0,
|
|
25
|
+
status: 'not_started',
|
|
26
|
+
color: 'BLUE',
|
|
27
|
+
created_at: '2026-06-12T00:00:00.000Z',
|
|
28
|
+
creator_id: 'user-1',
|
|
29
|
+
archived: false,
|
|
30
|
+
deleted: false,
|
|
31
|
+
} satisfies TaskList;
|
|
32
|
+
|
|
33
|
+
describe('getTaskCardHydratingOpenOptions', () => {
|
|
34
|
+
it('opens external task cards from the visible snapshot while hydrating source details', () => {
|
|
35
|
+
const externalTask = {
|
|
36
|
+
...task,
|
|
37
|
+
list_id: 'personal-list',
|
|
38
|
+
source_workspace_id: 'source-workspace',
|
|
39
|
+
source_workspace_name: 'Source workspace',
|
|
40
|
+
source_board_id: 'source-board',
|
|
41
|
+
source_board_name: 'Source board',
|
|
42
|
+
source_list_id: 'source-list',
|
|
43
|
+
source_list_name: 'Doing',
|
|
44
|
+
source_list_status: 'active',
|
|
45
|
+
ticket_prefix: 'SRC',
|
|
46
|
+
} satisfies Task & { ticket_prefix: string };
|
|
47
|
+
|
|
48
|
+
expect(
|
|
49
|
+
getTaskCardHydratingOpenOptions({
|
|
50
|
+
task: externalTask,
|
|
51
|
+
boardId: 'personal-board',
|
|
52
|
+
availableLists: [list],
|
|
53
|
+
effectiveWorkspaceId: 'personal-workspace',
|
|
54
|
+
isPersonalWorkspace: true,
|
|
55
|
+
})
|
|
56
|
+
).toEqual({
|
|
57
|
+
initialTask: {
|
|
58
|
+
...externalTask,
|
|
59
|
+
list_id: 'source-list',
|
|
60
|
+
},
|
|
61
|
+
boardId: 'source-board',
|
|
62
|
+
availableLists: [
|
|
63
|
+
{
|
|
64
|
+
id: 'source-list',
|
|
65
|
+
name: 'Doing',
|
|
66
|
+
board_id: 'source-board',
|
|
67
|
+
position: 0,
|
|
68
|
+
status: 'active',
|
|
69
|
+
color: 'GRAY',
|
|
70
|
+
created_at: '2026-06-12T00:00:00.000Z',
|
|
71
|
+
creator_id: '',
|
|
72
|
+
archived: false,
|
|
73
|
+
deleted: false,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
taskWsId: 'source-workspace',
|
|
77
|
+
taskWorkspacePersonal: false,
|
|
78
|
+
initialSharedContext: {
|
|
79
|
+
boardConfig: {
|
|
80
|
+
id: 'source-board',
|
|
81
|
+
name: 'Source board',
|
|
82
|
+
ws_id: 'source-workspace',
|
|
83
|
+
ticket_prefix: 'SRC',
|
|
84
|
+
},
|
|
85
|
+
availableLists: [
|
|
86
|
+
{
|
|
87
|
+
id: 'source-list',
|
|
88
|
+
name: 'Doing',
|
|
89
|
+
board_id: 'source-board',
|
|
90
|
+
position: 0,
|
|
91
|
+
status: 'active',
|
|
92
|
+
color: 'GRAY',
|
|
93
|
+
created_at: '2026-06-12T00:00:00.000Z',
|
|
94
|
+
creator_id: '',
|
|
95
|
+
archived: false,
|
|
96
|
+
deleted: false,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
workspaceLabels: [],
|
|
100
|
+
workspaceMembers: [],
|
|
101
|
+
workspaceProjects: [],
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('keeps local board metadata for non-source personal tasks', () => {
|
|
107
|
+
expect(
|
|
108
|
+
getTaskCardHydratingOpenOptions({
|
|
109
|
+
task,
|
|
110
|
+
boardId: 'board-1',
|
|
111
|
+
availableLists: [list],
|
|
112
|
+
effectiveWorkspaceId: 'workspace-1',
|
|
113
|
+
isPersonalWorkspace: true,
|
|
114
|
+
})
|
|
115
|
+
).toEqual({
|
|
116
|
+
initialTask: task,
|
|
117
|
+
boardId: 'board-1',
|
|
118
|
+
availableLists: [list],
|
|
119
|
+
taskWsId: 'workspace-1',
|
|
120
|
+
taskWorkspacePersonal: true,
|
|
121
|
+
initialSharedContext: undefined,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('treats source metadata as an external task snapshot', () => {
|
|
126
|
+
expect(
|
|
127
|
+
isExternalTaskSnapshot({
|
|
128
|
+
...task,
|
|
129
|
+
source_workspace_id: 'source-workspace',
|
|
130
|
+
})
|
|
131
|
+
).toBe(true);
|
|
132
|
+
expect(isExternalTaskSnapshot(task)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
|
+
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
3
|
+
import type { SharedTaskContext } from '../../../shared/task-edit-dialog/hooks/use-task-data';
|
|
4
|
+
|
|
5
|
+
interface TaskCardOpenOptionsInput {
|
|
6
|
+
task: Task;
|
|
7
|
+
boardId: string;
|
|
8
|
+
availableLists?: TaskList[];
|
|
9
|
+
effectiveWorkspaceId?: string;
|
|
10
|
+
isPersonalWorkspace: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isExternalTaskSnapshot(task: Task) {
|
|
14
|
+
return (
|
|
15
|
+
task.is_personal_external === true ||
|
|
16
|
+
Boolean(task.personal_board_id) ||
|
|
17
|
+
Boolean(task.source_workspace_id) ||
|
|
18
|
+
Boolean(task.source_board_id)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeSourceListStatus(status?: string | null): TaskList['status'] {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'documents':
|
|
25
|
+
case 'not_started':
|
|
26
|
+
case 'active':
|
|
27
|
+
case 'review':
|
|
28
|
+
case 'done':
|
|
29
|
+
case 'closed':
|
|
30
|
+
return status;
|
|
31
|
+
default:
|
|
32
|
+
return 'not_started';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getTaskTicketPrefix(task: Task) {
|
|
37
|
+
return 'ticket_prefix' in task && typeof task.ticket_prefix === 'string'
|
|
38
|
+
? task.ticket_prefix
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildInitialSourceList(
|
|
43
|
+
task: Task,
|
|
44
|
+
sourceBoardId: string
|
|
45
|
+
): TaskList | undefined {
|
|
46
|
+
if (!task.source_list_id) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: task.source_list_id,
|
|
52
|
+
name: task.source_list_name ?? task.source_list_id,
|
|
53
|
+
archived: false,
|
|
54
|
+
deleted: false,
|
|
55
|
+
created_at: task.created_at,
|
|
56
|
+
board_id: sourceBoardId,
|
|
57
|
+
creator_id: '',
|
|
58
|
+
status: normalizeSourceListStatus(task.source_list_status),
|
|
59
|
+
color: 'GRAY',
|
|
60
|
+
position: 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildInitialSourceContext(
|
|
65
|
+
task: Task,
|
|
66
|
+
sourceWorkspaceId?: string,
|
|
67
|
+
sourceBoardId?: string
|
|
68
|
+
): SharedTaskContext | undefined {
|
|
69
|
+
if (!sourceBoardId) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sourceList = buildInitialSourceList(task, sourceBoardId);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
boardConfig: {
|
|
77
|
+
id: sourceBoardId,
|
|
78
|
+
name: task.source_board_name ?? sourceBoardId,
|
|
79
|
+
ws_id: sourceWorkspaceId,
|
|
80
|
+
ticket_prefix: getTaskTicketPrefix(task),
|
|
81
|
+
},
|
|
82
|
+
availableLists: sourceList ? [sourceList] : undefined,
|
|
83
|
+
workspaceLabels: task.labels ?? [],
|
|
84
|
+
workspaceMembers:
|
|
85
|
+
task.assignees?.map((assignee) => ({
|
|
86
|
+
id: assignee.id,
|
|
87
|
+
user_id: assignee.id,
|
|
88
|
+
display_name: assignee.display_name ?? assignee.email ?? assignee.id,
|
|
89
|
+
avatar_url: assignee.avatar_url ?? null,
|
|
90
|
+
})) ?? [],
|
|
91
|
+
workspaceProjects: task.projects ?? [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getTaskCardHydratingOpenOptions({
|
|
96
|
+
task,
|
|
97
|
+
boardId,
|
|
98
|
+
availableLists,
|
|
99
|
+
effectiveWorkspaceId,
|
|
100
|
+
isPersonalWorkspace,
|
|
101
|
+
}: TaskCardOpenOptionsInput) {
|
|
102
|
+
const sourceWorkspaceId = task.source_workspace_id;
|
|
103
|
+
const sourceBoardId = task.source_board_id;
|
|
104
|
+
const initialSharedContext = buildInitialSourceContext(
|
|
105
|
+
task,
|
|
106
|
+
sourceWorkspaceId ?? undefined,
|
|
107
|
+
sourceBoardId ?? undefined
|
|
108
|
+
);
|
|
109
|
+
const initialTask = {
|
|
110
|
+
...task,
|
|
111
|
+
list_id: task.source_list_id ?? task.list_id,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
initialTask,
|
|
116
|
+
boardId: sourceBoardId ?? boardId,
|
|
117
|
+
availableLists:
|
|
118
|
+
sourceBoardId && initialSharedContext?.availableLists
|
|
119
|
+
? initialSharedContext.availableLists
|
|
120
|
+
: sourceBoardId
|
|
121
|
+
? undefined
|
|
122
|
+
: availableLists,
|
|
123
|
+
taskWsId: sourceWorkspaceId ?? effectiveWorkspaceId,
|
|
124
|
+
taskWorkspacePersonal: sourceWorkspaceId ? false : isPersonalWorkspace,
|
|
125
|
+
initialSharedContext,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -144,6 +144,10 @@ import { areTaskCardPropsEqual } from './task-card-comparator';
|
|
|
144
144
|
import { shouldRenderTaskCardCompletionCheckbox } from './task-card-completion-checkbox-visibility';
|
|
145
145
|
import { TaskCardIdentifierRow } from './task-card-identifier-row';
|
|
146
146
|
import { mergeTaskCardLabelOptions } from './task-card-label-options';
|
|
147
|
+
import {
|
|
148
|
+
getTaskCardHydratingOpenOptions,
|
|
149
|
+
isExternalTaskSnapshot,
|
|
150
|
+
} from './task-card-open-options';
|
|
147
151
|
import { getTaskCardVisibilityState } from './task-card-visibility';
|
|
148
152
|
import { TaskSchedulingBadge } from './task-scheduling-badge';
|
|
149
153
|
|
|
@@ -662,8 +666,7 @@ function TaskCardInner({
|
|
|
662
666
|
|
|
663
667
|
// Check if task is optimistically added (pending realtime confirmation)
|
|
664
668
|
const isOptimistic = '_isOptimistic' in task && task._isOptimistic === true;
|
|
665
|
-
const isPersonalExternalTask =
|
|
666
|
-
task.is_personal_external === true || Boolean(task.personal_board_id);
|
|
669
|
+
const isPersonalExternalTask = isExternalTaskSnapshot(task);
|
|
667
670
|
const sourceBoardUrl =
|
|
668
671
|
task.source_workspace_id && task.source_board_id
|
|
669
672
|
? `/${task.source_workspace_id}${tasksHref(`/boards/${task.source_board_id}`)}`
|
|
@@ -887,51 +890,23 @@ function TaskCardInner({
|
|
|
887
890
|
// Removed explicit drag handle – entire card is now draggable for better UX.
|
|
888
891
|
// Keep attributes/listeners to spread onto root interactive area.
|
|
889
892
|
|
|
890
|
-
const openExternalTask = useCallback(
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
try {
|
|
903
|
-
sourceLists = (
|
|
904
|
-
await listWorkspaceTaskLists(sourceWorkspaceId, sourceBoardId)
|
|
905
|
-
).lists;
|
|
906
|
-
} catch {
|
|
907
|
-
sourceLists = undefined;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
openTask(sourceTask as Task, sourceBoardId, sourceLists, false, {
|
|
911
|
-
taskWsId: sourceWorkspaceId,
|
|
912
|
-
taskWorkspacePersonal: false,
|
|
913
|
-
});
|
|
914
|
-
return;
|
|
915
|
-
} catch {
|
|
916
|
-
const opened = await openTaskById(task.id);
|
|
917
|
-
if (opened) return;
|
|
918
|
-
}
|
|
919
|
-
} else {
|
|
920
|
-
const opened = await openTaskById(task.id);
|
|
921
|
-
if (opened) return;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
openTask(task, sourceBoardId ?? boardId, availableLists, false, {
|
|
925
|
-
taskWsId: sourceWorkspaceId ?? effectiveWorkspaceId,
|
|
926
|
-
taskWorkspacePersonal: sourceWorkspaceId ? false : isPersonalWorkspace,
|
|
927
|
-
});
|
|
893
|
+
const openExternalTask = useCallback(() => {
|
|
894
|
+
void openTaskById(
|
|
895
|
+
task.id,
|
|
896
|
+
getTaskCardHydratingOpenOptions({
|
|
897
|
+
task,
|
|
898
|
+
boardId,
|
|
899
|
+
availableLists,
|
|
900
|
+
effectiveWorkspaceId,
|
|
901
|
+
isPersonalWorkspace,
|
|
902
|
+
})
|
|
903
|
+
);
|
|
928
904
|
}, [
|
|
929
905
|
task,
|
|
930
906
|
boardId,
|
|
931
907
|
availableLists,
|
|
932
908
|
effectiveWorkspaceId,
|
|
933
909
|
isPersonalWorkspace,
|
|
934
|
-
openTask,
|
|
935
910
|
openTaskById,
|
|
936
911
|
]);
|
|
937
912
|
|
|
@@ -956,7 +931,7 @@ function TaskCardInner({
|
|
|
956
931
|
) {
|
|
957
932
|
// Only open edit dialog if not in multi-select mode, not dragging, and no other dialogs are open
|
|
958
933
|
if (isPersonalExternalTask) {
|
|
959
|
-
|
|
934
|
+
openExternalTask();
|
|
960
935
|
return;
|
|
961
936
|
}
|
|
962
937
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
4
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
5
|
+
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { TimelineBoard } from './timeline-board';
|
|
8
|
+
|
|
9
|
+
const createTaskMock = vi.hoisted(() => vi.fn());
|
|
10
|
+
const openTaskMock = vi.hoisted(() => vi.fn());
|
|
11
|
+
const openTaskByIdMock = vi.hoisted(() => vi.fn());
|
|
12
|
+
const updateWorkspaceTaskMock = vi.hoisted(() => vi.fn());
|
|
13
|
+
const deleteWorkspaceTaskMock = vi.hoisted(() => vi.fn());
|
|
14
|
+
|
|
15
|
+
class MockResizeObserver {
|
|
16
|
+
observe() {}
|
|
17
|
+
unobserve() {}
|
|
18
|
+
disconnect() {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
globalThis.ResizeObserver = MockResizeObserver as typeof ResizeObserver;
|
|
22
|
+
|
|
23
|
+
vi.mock('next-intl', () => ({
|
|
24
|
+
useLocale: () => 'en',
|
|
25
|
+
useTranslations: () => (key: string) => key,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('@tuturuuu/internal-api', () => ({
|
|
29
|
+
updateWorkspaceTask: updateWorkspaceTaskMock,
|
|
30
|
+
deleteWorkspaceTask: deleteWorkspaceTaskMock,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock('../../hooks/useTaskDialog', () => ({
|
|
34
|
+
useTaskDialog: () => ({
|
|
35
|
+
createTask: createTaskMock,
|
|
36
|
+
openTask: openTaskMock,
|
|
37
|
+
openTaskById: openTaskByIdMock,
|
|
38
|
+
}),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
vi.mock('./timeline/timeline-grid', () => ({
|
|
42
|
+
TimelineGrid: ({
|
|
43
|
+
localTasks,
|
|
44
|
+
onOpenTask,
|
|
45
|
+
}: {
|
|
46
|
+
localTasks: Task[];
|
|
47
|
+
onOpenTask: (task: Task) => void;
|
|
48
|
+
}) => (
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
data-testid="open-timeline-task"
|
|
52
|
+
onClick={() => onOpenTask(localTasks[0]!)}
|
|
53
|
+
>
|
|
54
|
+
Open timeline task
|
|
55
|
+
</button>
|
|
56
|
+
),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const lists: TaskList[] = [
|
|
60
|
+
{
|
|
61
|
+
archived: false,
|
|
62
|
+
board_id: 'board-1',
|
|
63
|
+
color: 'GRAY',
|
|
64
|
+
created_at: '2026-05-01T00:00:00.000Z',
|
|
65
|
+
creator_id: 'user-1',
|
|
66
|
+
deleted: false,
|
|
67
|
+
id: 'todo',
|
|
68
|
+
name: 'To Do',
|
|
69
|
+
position: 0,
|
|
70
|
+
status: 'not_started',
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function task(overrides: Partial<Task> & Pick<Task, 'id' | 'name'>): Task {
|
|
75
|
+
return {
|
|
76
|
+
created_at: '2026-05-01T00:00:00.000Z',
|
|
77
|
+
display_number: 1,
|
|
78
|
+
end_date: null,
|
|
79
|
+
labels: [],
|
|
80
|
+
list_id: 'todo',
|
|
81
|
+
priority: 'normal',
|
|
82
|
+
sort_key: 1,
|
|
83
|
+
start_date: null,
|
|
84
|
+
...overrides,
|
|
85
|
+
} as Task;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderTimeline(tasks: Task[]) {
|
|
89
|
+
const queryClient = new QueryClient({
|
|
90
|
+
defaultOptions: {
|
|
91
|
+
mutations: { retry: false },
|
|
92
|
+
queries: { retry: false },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return render(
|
|
97
|
+
<QueryClientProvider client={queryClient}>
|
|
98
|
+
<TimelineBoard
|
|
99
|
+
boardId="board-1"
|
|
100
|
+
lists={lists}
|
|
101
|
+
tasks={tasks}
|
|
102
|
+
wsId="ws-1"
|
|
103
|
+
/>
|
|
104
|
+
</QueryClientProvider>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe('TimelineBoard task opening', () => {
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
createTaskMock.mockReset();
|
|
111
|
+
openTaskMock.mockReset();
|
|
112
|
+
openTaskByIdMock.mockReset();
|
|
113
|
+
updateWorkspaceTaskMock.mockReset();
|
|
114
|
+
deleteWorkspaceTaskMock.mockReset();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('opens external source tasks through the hydrating task-by-id path immediately', () => {
|
|
118
|
+
renderTimeline([
|
|
119
|
+
task({
|
|
120
|
+
id: 'external-timeline-task',
|
|
121
|
+
name: 'External timeline task',
|
|
122
|
+
list_id: 'personal-list',
|
|
123
|
+
personal_board_id: 'board-1',
|
|
124
|
+
is_personal_external: true,
|
|
125
|
+
source_workspace_id: 'source-ws',
|
|
126
|
+
source_board_id: 'source-board',
|
|
127
|
+
source_board_name: 'Source board',
|
|
128
|
+
source_list_id: 'source-list',
|
|
129
|
+
source_list_name: 'Source list',
|
|
130
|
+
}),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
fireEvent.click(screen.getByTestId('open-timeline-task'));
|
|
134
|
+
|
|
135
|
+
expect(openTaskByIdMock).toHaveBeenCalledWith(
|
|
136
|
+
'external-timeline-task',
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
boardId: 'source-board',
|
|
139
|
+
taskWsId: 'source-ws',
|
|
140
|
+
taskWorkspacePersonal: false,
|
|
141
|
+
initialTask: expect.objectContaining({
|
|
142
|
+
id: 'external-timeline-task',
|
|
143
|
+
list_id: 'source-list',
|
|
144
|
+
name: 'External timeline task',
|
|
145
|
+
}),
|
|
146
|
+
initialSharedContext: expect.objectContaining({
|
|
147
|
+
boardConfig: expect.objectContaining({
|
|
148
|
+
id: 'source-board',
|
|
149
|
+
name: 'Source board',
|
|
150
|
+
ws_id: 'source-ws',
|
|
151
|
+
}),
|
|
152
|
+
availableLists: [
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
id: 'source-list',
|
|
155
|
+
name: 'Source list',
|
|
156
|
+
board_id: 'source-board',
|
|
157
|
+
}),
|
|
158
|
+
],
|
|
159
|
+
}),
|
|
160
|
+
})
|
|
161
|
+
);
|
|
162
|
+
expect(openTaskMock).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -32,6 +32,10 @@ import {
|
|
|
32
32
|
useState,
|
|
33
33
|
} from 'react';
|
|
34
34
|
import { useTaskDialog } from '../../hooks/useTaskDialog';
|
|
35
|
+
import {
|
|
36
|
+
getTaskCardHydratingOpenOptions,
|
|
37
|
+
isExternalTaskSnapshot,
|
|
38
|
+
} from './task-card/task-card-open-options';
|
|
35
39
|
import { TaskEditDialog } from './timeline/task-edit-dialog';
|
|
36
40
|
import {
|
|
37
41
|
DEFAULT_DAY_WIDTH,
|
|
@@ -111,7 +115,7 @@ export function TimelineBoard({
|
|
|
111
115
|
}: TimelineProps) {
|
|
112
116
|
const t = useTranslations('common');
|
|
113
117
|
const locale = useLocale();
|
|
114
|
-
const { createTask, openTask } = useTaskDialog();
|
|
118
|
+
const { createTask, openTask, openTaskById } = useTaskDialog();
|
|
115
119
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
116
120
|
const [dayWidth, setDayWidth] = useState(DEFAULT_DAY_WIDTH);
|
|
117
121
|
const [density, setDensity] = useState<Density>('comfortable');
|
|
@@ -226,23 +230,28 @@ export function TimelineBoard({
|
|
|
226
230
|
|
|
227
231
|
const openTimelineTask = useCallback(
|
|
228
232
|
(task: Task) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
if (!boardId) return;
|
|
234
|
+
|
|
235
|
+
if (isExternalTaskSnapshot(task)) {
|
|
236
|
+
void openTaskById(
|
|
237
|
+
task.id,
|
|
238
|
+
getTaskCardHydratingOpenOptions({
|
|
239
|
+
task,
|
|
240
|
+
boardId,
|
|
241
|
+
availableLists: lists,
|
|
242
|
+
effectiveWorkspaceId: wsId,
|
|
243
|
+
isPersonalWorkspace: Boolean(wsId),
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
233
248
|
|
|
234
|
-
openTask(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
false,
|
|
239
|
-
{
|
|
240
|
-
taskWsId: targetWorkspaceId,
|
|
241
|
-
taskWorkspacePersonal: Boolean(wsId) && !task.source_workspace_id,
|
|
242
|
-
}
|
|
243
|
-
);
|
|
249
|
+
openTask(task, boardId, lists, false, {
|
|
250
|
+
taskWsId: wsId,
|
|
251
|
+
taskWorkspacePersonal: Boolean(wsId),
|
|
252
|
+
});
|
|
244
253
|
},
|
|
245
|
-
[boardId, lists, openTask, wsId]
|
|
254
|
+
[boardId, lists, openTask, openTaskById, wsId]
|
|
246
255
|
);
|
|
247
256
|
|
|
248
257
|
const clearDraft = useCallback((taskId: string) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkspaceProductTier } from '@tuturuuu/types';
|
|
1
2
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
3
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
3
4
|
import type { TaskFilters } from '@tuturuuu/ui/tu-do/boards/boardId/task-filter';
|
|
@@ -5,6 +6,7 @@ import {
|
|
|
5
6
|
type PendingRelationshipType,
|
|
6
7
|
useTaskDialogContext,
|
|
7
8
|
} from '../providers/task-dialog-provider';
|
|
9
|
+
import type { SharedTaskContext } from '../shared/task-edit-dialog/hooks/use-task-data';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Hook to open and manage the centralized task dialog
|
|
@@ -50,7 +52,19 @@ export function useTaskDialog(): {
|
|
|
50
52
|
taskWorkspacePersonal?: boolean;
|
|
51
53
|
}
|
|
52
54
|
) => void;
|
|
53
|
-
openTaskById: (
|
|
55
|
+
openTaskById: (
|
|
56
|
+
taskId: string,
|
|
57
|
+
options?: {
|
|
58
|
+
initialTask?: Partial<Task>;
|
|
59
|
+
boardId?: string;
|
|
60
|
+
availableLists?: TaskList[];
|
|
61
|
+
fakeTaskUrl?: boolean;
|
|
62
|
+
taskWsId?: string;
|
|
63
|
+
taskWorkspacePersonal?: boolean;
|
|
64
|
+
taskWorkspaceTier?: WorkspaceProductTier;
|
|
65
|
+
initialSharedContext?: SharedTaskContext;
|
|
66
|
+
}
|
|
67
|
+
) => Promise<boolean>;
|
|
54
68
|
createTask: (
|
|
55
69
|
boardId: string,
|
|
56
70
|
listId: string,
|
|
@@ -7,6 +7,7 @@ import { useTaskContextActions } from '../use-task-context-actions';
|
|
|
7
7
|
const {
|
|
8
8
|
mockAddWorkspaceTaskLabel,
|
|
9
9
|
mockDeleteWorkspaceTask,
|
|
10
|
+
mockDispatchTaskSoundCue,
|
|
10
11
|
mockInvalidateQueries,
|
|
11
12
|
mockListWorkspaceTaskLists,
|
|
12
13
|
mockRemoveWorkspaceTaskLabel,
|
|
@@ -17,6 +18,7 @@ const {
|
|
|
17
18
|
} = vi.hoisted(() => ({
|
|
18
19
|
mockAddWorkspaceTaskLabel: vi.fn(),
|
|
19
20
|
mockDeleteWorkspaceTask: vi.fn(),
|
|
21
|
+
mockDispatchTaskSoundCue: vi.fn(),
|
|
20
22
|
mockInvalidateQueries: vi.fn(),
|
|
21
23
|
mockListWorkspaceTaskLists: vi.fn(),
|
|
22
24
|
mockRemoveWorkspaceTaskLabel: vi.fn(),
|
|
@@ -55,6 +57,10 @@ vi.mock('@tuturuuu/ui/sonner', () => ({
|
|
|
55
57
|
},
|
|
56
58
|
}));
|
|
57
59
|
|
|
60
|
+
vi.mock('../../shared/task-sound-effects', () => ({
|
|
61
|
+
dispatchTaskSoundCue: mockDispatchTaskSoundCue,
|
|
62
|
+
}));
|
|
63
|
+
|
|
58
64
|
// Mock fetch
|
|
59
65
|
global.fetch = vi.fn();
|
|
60
66
|
|
|
@@ -131,6 +137,7 @@ describe('useTaskContextActions', () => {
|
|
|
131
137
|
);
|
|
132
138
|
expect(onTaskUpdate).toHaveBeenCalled();
|
|
133
139
|
expect(onClose).toHaveBeenCalled();
|
|
140
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('complete');
|
|
134
141
|
});
|
|
135
142
|
|
|
136
143
|
it('handleUndoDoneWithMyPart sends PUT with both flags cleared', async () => {
|
|
@@ -165,6 +172,7 @@ describe('useTaskContextActions', () => {
|
|
|
165
172
|
);
|
|
166
173
|
expect(onTaskUpdate).toHaveBeenCalled();
|
|
167
174
|
expect(onClose).toHaveBeenCalled();
|
|
175
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('update');
|
|
168
176
|
});
|
|
169
177
|
|
|
170
178
|
it('handleComplete clears overrides when task has completed_at override', async () => {
|
|
@@ -279,6 +287,7 @@ describe('useTaskContextActions', () => {
|
|
|
279
287
|
expect(mockDeleteWorkspaceTask).toHaveBeenCalledWith('ws-1', mockTask.id);
|
|
280
288
|
expect(onTaskUpdate).toHaveBeenCalled();
|
|
281
289
|
expect(onClose).toHaveBeenCalled();
|
|
290
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('delete');
|
|
282
291
|
});
|
|
283
292
|
|
|
284
293
|
it('handlePriorityChange updates task priority via internal API', async () => {
|
|
@@ -299,6 +308,7 @@ describe('useTaskContextActions', () => {
|
|
|
299
308
|
priority: 'high',
|
|
300
309
|
});
|
|
301
310
|
expect(mockInvalidateQueries).toHaveBeenCalled();
|
|
311
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('update');
|
|
302
312
|
});
|
|
303
313
|
|
|
304
314
|
it('handleUnassignMe updates assignee_ids via internal API', async () => {
|
|
@@ -512,5 +522,6 @@ describe('useTaskContextActions', () => {
|
|
|
512
522
|
});
|
|
513
523
|
expect(onTaskUpdate).toHaveBeenCalled();
|
|
514
524
|
expect(onClose).toHaveBeenCalled();
|
|
525
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('move');
|
|
515
526
|
});
|
|
516
527
|
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '@tuturuuu/internal-api';
|
|
17
17
|
import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
|
|
18
18
|
import { AI_CREDITS_QUERY_KEY } from '@tuturuuu/ui/hooks/use-ai-credits';
|
|
19
|
+
import { useTaskUserRealtime } from '@tuturuuu/ui/hooks/useTaskUserRealtime';
|
|
19
20
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
20
21
|
import { useTaskDialog } from '@tuturuuu/ui/tu-do/hooks/useTaskDialog';
|
|
21
22
|
import { useBoardConfig } from '@tuturuuu/utils/task-helper';
|
|
@@ -51,6 +52,7 @@ export function useMyTasksState({
|
|
|
51
52
|
const t = useTranslations();
|
|
52
53
|
const queryClient = useQueryClient();
|
|
53
54
|
const { onUpdate, openTaskById } = useTaskDialog();
|
|
55
|
+
useTaskUserRealtime(userId);
|
|
54
56
|
|
|
55
57
|
// Filter state (declared before query so it can be passed as param)
|
|
56
58
|
const [taskFilters, setTaskFilters] = useState<{
|