@tuturuuu/ui 0.14.0 → 0.16.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 +78 -0
- package/biome.json +1 -1
- package/package.json +62 -62
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +12 -0
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +93 -6
- package/src/components/ui/custom/loading-statistic-card.tsx +18 -0
- package/src/components/ui/custom/notification-popover-client.tsx +5 -1
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.test.tsx +34 -0
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +106 -0
- package/src/components/ui/custom/settings/sidebar-settings.tsx +18 -6
- package/src/components/ui/custom/settings-dialog-shell.tsx +18 -12
- package/src/components/ui/custom/sidebar-context.tsx +58 -3
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +5 -3
- package/src/components/ui/custom/statistic-card.tsx +104 -0
- package/src/components/ui/custom/structure.test.tsx +29 -0
- package/src/components/ui/custom/structure.tsx +103 -88
- package/src/components/ui/finance/invoices/civil-month.ts +224 -0
- package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.test.tsx +49 -0
- package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.tsx +81 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +90 -2
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +40 -4
- package/src/components/ui/finance/invoices/pending-columns.test.ts +26 -0
- package/src/components/ui/finance/invoices/pending-columns.tsx +45 -14
- package/src/components/ui/finance/invoices/standard-invoice.test.ts +31 -0
- package/src/components/ui/finance/invoices/standard-invoice.tsx +33 -16
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +331 -0
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +53 -28
- package/src/components/ui/finance/invoices/utils.test.ts +84 -0
- package/src/components/ui/finance/invoices/utils.ts +56 -134
- package/src/components/ui/resizable.test.ts +15 -0
- package/src/components/ui/resizable.tsx +28 -6
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +62 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +48 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +32 -2
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +41 -3
- package/src/components/ui/tu-do/boards/boardId/menus/task-due-date-menu.tsx +31 -10
- package/src/components/ui/tu-do/boards/boardId/menus/task-priority-menu.tsx +7 -5
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +7 -5
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.test.ts +44 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +44 -30
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +54 -0
- package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +43 -1
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +33 -15
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +3 -3
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +74 -28
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +12 -10
- package/src/components/ui/tu-do/projects/components/project-grid-card.tsx +22 -6
- package/src/components/ui/tu-do/projects/components/project-list-item.tsx +24 -8
- package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx +6 -0
- package/src/components/ui/tu-do/projects/projectId/components/tasks-tab.tsx +3 -1
- package/src/components/ui/tu-do/projects/projectId/task-project-detail-page-client.tsx +11 -3
- package/src/components/ui/tu-do/projects/projectId/task-project-detail.tsx +2 -0
- package/src/components/ui/tu-do/projects/projectId/types.ts +1 -0
- package/src/components/ui/tu-do/projects/task-projects-client.tsx +45 -1
- package/src/components/ui/tu-do/projects/types.ts +4 -1
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +10 -0
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +87 -2
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +27 -3
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +41 -3
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +54 -0
- package/src/components/ui/tu-do/shared/board-header.tsx +20 -0
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +2 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +7 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-mutations.test.tsx +231 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +139 -68
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.test.tsx +68 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +17 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +45 -23
- package/src/components/ui/tu-do/shared/task-row-actions-menu.test.tsx +138 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +15 -9
- package/src/components/ui/tu-do/templates/client.tsx +97 -78
- package/src/components/ui/tu-do/templates/marketplace/client.tsx +171 -135
- package/src/components/ui/tu-do/templates/task-templates-hub.tsx +3 -0
- package/src/components/ui/tu-do/templates/templateId/client.tsx +24 -9
- package/src/components/ui/tu-do/templates/templateId/task-template-detail-page-client.tsx +8 -2
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +14 -39
- package/src/hooks/task-actions-personal-external.ts +12 -6
- package/src/hooks/use-notifications.ts +14 -2
- package/vitest.config.ts +4 -0
package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-mutations.test.tsx
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { act, renderHook } from '@testing-library/react';
|
|
3
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { updateWorkspaceTask } from '../task-api';
|
|
7
|
+
import {
|
|
8
|
+
type UseTaskMutationsProps,
|
|
9
|
+
useTaskMutations,
|
|
10
|
+
} from '../use-task-mutations';
|
|
11
|
+
|
|
12
|
+
vi.mock('../task-api', () => ({
|
|
13
|
+
updateWorkspaceTask: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('@tuturuuu/ui/hooks/use-toast', () => ({
|
|
17
|
+
useToast: () => ({ toast: vi.fn() }),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('useTaskMutations', () => {
|
|
21
|
+
let queryClient: QueryClient;
|
|
22
|
+
|
|
23
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
24
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const personalOverlayTask = {
|
|
28
|
+
id: 'task-1',
|
|
29
|
+
name: 'Overlay task',
|
|
30
|
+
description: '',
|
|
31
|
+
display_number: 1,
|
|
32
|
+
created_at: '2026-07-07T00:00:00.000Z',
|
|
33
|
+
list_id: 'personal-list',
|
|
34
|
+
personal_board_id: 'personal-board',
|
|
35
|
+
personal_list_id: 'personal-list',
|
|
36
|
+
personal_sort_key: 1234,
|
|
37
|
+
sort_key: 1234,
|
|
38
|
+
source_workspace_id: 'source-ws',
|
|
39
|
+
source_board_id: 'source-board',
|
|
40
|
+
source_list_id: 'source-list',
|
|
41
|
+
source_list_name: 'Source list',
|
|
42
|
+
source_list_status: 'active',
|
|
43
|
+
is_personal_external: true,
|
|
44
|
+
priority: 'normal',
|
|
45
|
+
estimation_points: 3,
|
|
46
|
+
end_date: null,
|
|
47
|
+
labels: [],
|
|
48
|
+
assignees: [],
|
|
49
|
+
projects: [],
|
|
50
|
+
} satisfies Task;
|
|
51
|
+
|
|
52
|
+
const sourceTask = {
|
|
53
|
+
...personalOverlayTask,
|
|
54
|
+
list_id: 'source-list',
|
|
55
|
+
personal_board_id: null,
|
|
56
|
+
personal_list_id: null,
|
|
57
|
+
personal_sort_key: null,
|
|
58
|
+
is_personal_external: false,
|
|
59
|
+
} satisfies Task;
|
|
60
|
+
|
|
61
|
+
function makeProps(
|
|
62
|
+
overrides: Partial<UseTaskMutationsProps> = {}
|
|
63
|
+
): UseTaskMutationsProps {
|
|
64
|
+
return {
|
|
65
|
+
wsId: 'source-ws',
|
|
66
|
+
taskId: 'task-1',
|
|
67
|
+
isCreateMode: false,
|
|
68
|
+
boardId: 'source-board',
|
|
69
|
+
visibleBoardId: 'personal-board',
|
|
70
|
+
visibleTaskSnapshot: personalOverlayTask,
|
|
71
|
+
estimationPoints: 3,
|
|
72
|
+
priority: 'normal',
|
|
73
|
+
selectedListId: 'source-list',
|
|
74
|
+
taskName: 'Overlay task',
|
|
75
|
+
setEstimationPoints: vi.fn(),
|
|
76
|
+
setPriority: vi.fn(),
|
|
77
|
+
setStartDate: vi.fn(),
|
|
78
|
+
setEndDate: vi.fn(),
|
|
79
|
+
setSelectedListId: vi.fn(),
|
|
80
|
+
fallbackBroadcast: null,
|
|
81
|
+
onUpdate: vi.fn(),
|
|
82
|
+
...overrides,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getPersonalOverlayCacheTask() {
|
|
87
|
+
return queryClient.getQueryData<Task[]>(['tasks', 'personal-board'])?.[0] as
|
|
88
|
+
| (Task & { _localMutationAt?: number })
|
|
89
|
+
| undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
queryClient = new QueryClient({
|
|
94
|
+
defaultOptions: {
|
|
95
|
+
queries: { retry: false },
|
|
96
|
+
mutations: { retry: false },
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
queryClient.setQueryData(
|
|
100
|
+
['tasks', 'personal-board'],
|
|
101
|
+
[personalOverlayTask]
|
|
102
|
+
);
|
|
103
|
+
vi.mocked(updateWorkspaceTask).mockReset();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
queryClient.clear();
|
|
108
|
+
vi.restoreAllMocks();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('keeps a personal external card in the personal board cache when priority changes', async () => {
|
|
112
|
+
const onUpdate = vi.fn();
|
|
113
|
+
vi.mocked(updateWorkspaceTask).mockResolvedValueOnce({
|
|
114
|
+
task: {
|
|
115
|
+
...sourceTask,
|
|
116
|
+
priority: 'high',
|
|
117
|
+
list_id: 'source-list',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const { result } = renderHook(
|
|
122
|
+
() => useTaskMutations(makeProps({ onUpdate })),
|
|
123
|
+
{ wrapper }
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
await act(async () => {
|
|
127
|
+
await result.current.updatePriority('high');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(updateWorkspaceTask).toHaveBeenCalledWith('source-ws', 'task-1', {
|
|
131
|
+
priority: 'high',
|
|
132
|
+
});
|
|
133
|
+
expect(onUpdate).not.toHaveBeenCalled();
|
|
134
|
+
expect(getPersonalOverlayCacheTask()).toMatchObject({
|
|
135
|
+
id: 'task-1',
|
|
136
|
+
priority: 'high',
|
|
137
|
+
list_id: 'personal-list',
|
|
138
|
+
personal_board_id: 'personal-board',
|
|
139
|
+
personal_list_id: 'personal-list',
|
|
140
|
+
personal_sort_key: 1234,
|
|
141
|
+
source_board_id: 'source-board',
|
|
142
|
+
source_list_id: 'source-list',
|
|
143
|
+
_localMutationAt: expect.any(Number),
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('preserves personal placement while updating date, estimate, and name fields', async () => {
|
|
148
|
+
const startIso = '2026-07-08T00:00:00.000Z';
|
|
149
|
+
const endIso = '2026-07-09T00:00:00.000Z';
|
|
150
|
+
const onUpdate = vi.fn();
|
|
151
|
+
|
|
152
|
+
vi.mocked(updateWorkspaceTask)
|
|
153
|
+
.mockResolvedValueOnce({
|
|
154
|
+
task: { ...sourceTask, start_date: startIso, list_id: 'source-list' },
|
|
155
|
+
})
|
|
156
|
+
.mockResolvedValueOnce({
|
|
157
|
+
task: { ...sourceTask, end_date: endIso, list_id: 'source-list' },
|
|
158
|
+
})
|
|
159
|
+
.mockResolvedValueOnce({
|
|
160
|
+
task: {
|
|
161
|
+
...sourceTask,
|
|
162
|
+
estimation_points: 8,
|
|
163
|
+
list_id: 'source-list',
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
.mockResolvedValueOnce({
|
|
167
|
+
task: { ...sourceTask, name: 'Renamed task', list_id: 'source-list' },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const { result } = renderHook(
|
|
171
|
+
() => useTaskMutations(makeProps({ onUpdate })),
|
|
172
|
+
{ wrapper }
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await act(async () => {
|
|
176
|
+
await result.current.updateStartDate(new Date(startIso));
|
|
177
|
+
await result.current.updateEndDate(new Date(endIso));
|
|
178
|
+
await result.current.updateEstimation(8);
|
|
179
|
+
await result.current.saveNameToDatabase('Renamed task');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(onUpdate).not.toHaveBeenCalled();
|
|
183
|
+
expect(getPersonalOverlayCacheTask()).toMatchObject({
|
|
184
|
+
id: 'task-1',
|
|
185
|
+
name: 'Renamed task',
|
|
186
|
+
list_id: 'personal-list',
|
|
187
|
+
personal_list_id: 'personal-list',
|
|
188
|
+
start_date: startIso,
|
|
189
|
+
end_date: endIso,
|
|
190
|
+
estimation_points: 8,
|
|
191
|
+
_localMutationAt: expect.any(Number),
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('keeps normal source-board property refresh behavior unchanged', async () => {
|
|
196
|
+
const onUpdate = vi.fn();
|
|
197
|
+
queryClient.setQueryData(['tasks', 'source-board'], [sourceTask]);
|
|
198
|
+
vi.mocked(updateWorkspaceTask).mockResolvedValueOnce({
|
|
199
|
+
task: { ...sourceTask, priority: 'high' },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const { result } = renderHook(
|
|
203
|
+
() =>
|
|
204
|
+
useTaskMutations(
|
|
205
|
+
makeProps({
|
|
206
|
+
onUpdate,
|
|
207
|
+
visibleBoardId: undefined,
|
|
208
|
+
visibleTaskSnapshot: undefined,
|
|
209
|
+
})
|
|
210
|
+
),
|
|
211
|
+
{ wrapper }
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await act(async () => {
|
|
215
|
+
await result.current.updatePriority('high');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const sourceCacheTask = queryClient.getQueryData<Task[]>([
|
|
219
|
+
'tasks',
|
|
220
|
+
'source-board',
|
|
221
|
+
])?.[0] as (Task & { _localMutationAt?: number }) | undefined;
|
|
222
|
+
|
|
223
|
+
expect(onUpdate).toHaveBeenCalledTimes(1);
|
|
224
|
+
expect(sourceCacheTask).toMatchObject({
|
|
225
|
+
id: 'task-1',
|
|
226
|
+
priority: 'high',
|
|
227
|
+
list_id: 'source-list',
|
|
228
|
+
});
|
|
229
|
+
expect(sourceCacheTask?._localMutationAt).toBeUndefined();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
updateCurrentUserTaskSchedulingSettings,
|
|
5
5
|
} from '@tuturuuu/internal-api';
|
|
6
6
|
import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
|
|
7
|
-
import type { CalendarHoursType } from '@tuturuuu/types/primitives/Task';
|
|
7
|
+
import type { CalendarHoursType, Task } from '@tuturuuu/types/primitives/Task';
|
|
8
8
|
import { useToast } from '@tuturuuu/ui/hooks/use-toast';
|
|
9
9
|
import { useCallback, useState } from 'react';
|
|
10
|
+
import { isPersonalExternalOverlayTask } from '../../../../../../lib/task-personal-external';
|
|
10
11
|
import {
|
|
11
12
|
type BoardBroadcastFn,
|
|
12
13
|
getActiveBroadcast,
|
|
@@ -33,6 +34,8 @@ export interface UseTaskMutationsProps {
|
|
|
33
34
|
taskId?: string;
|
|
34
35
|
isCreateMode: boolean;
|
|
35
36
|
boardId: string;
|
|
37
|
+
visibleBoardId?: string;
|
|
38
|
+
visibleTaskSnapshot?: Partial<Task>;
|
|
36
39
|
estimationPoints: number | null;
|
|
37
40
|
priority: TaskPriority | null;
|
|
38
41
|
selectedListId?: string;
|
|
@@ -66,6 +69,27 @@ export interface UseTaskMutationsReturn {
|
|
|
66
69
|
schedulingSaving: boolean;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
type PropertyPatch = Pick<
|
|
73
|
+
Partial<Task>,
|
|
74
|
+
'name' | 'priority' | 'start_date' | 'end_date' | 'estimation_points'
|
|
75
|
+
>;
|
|
76
|
+
|
|
77
|
+
function hasPersonalExternalVisibleContext({
|
|
78
|
+
boardId,
|
|
79
|
+
visibleBoardId,
|
|
80
|
+
visibleTaskSnapshot,
|
|
81
|
+
}: {
|
|
82
|
+
boardId: string;
|
|
83
|
+
visibleBoardId?: string;
|
|
84
|
+
visibleTaskSnapshot?: Partial<Task>;
|
|
85
|
+
}) {
|
|
86
|
+
return Boolean(
|
|
87
|
+
visibleBoardId &&
|
|
88
|
+
visibleBoardId !== boardId &&
|
|
89
|
+
isPersonalExternalOverlayTask(visibleTaskSnapshot as Task | undefined)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
/**
|
|
70
94
|
* Custom hook for task database mutations (CRUD operations on task properties)
|
|
71
95
|
* Extracted from task-edit-dialog.tsx to improve maintainability
|
|
@@ -75,6 +99,8 @@ export function useTaskMutations({
|
|
|
75
99
|
taskId,
|
|
76
100
|
isCreateMode,
|
|
77
101
|
boardId,
|
|
102
|
+
visibleBoardId,
|
|
103
|
+
visibleTaskSnapshot,
|
|
78
104
|
estimationPoints,
|
|
79
105
|
priority,
|
|
80
106
|
selectedListId,
|
|
@@ -94,16 +120,57 @@ export function useTaskMutations({
|
|
|
94
120
|
contextBroadcast ?? getActiveBroadcast() ?? fallbackBroadcast;
|
|
95
121
|
const [estimationSaving, setEstimationSaving] = useState(false);
|
|
96
122
|
const [schedulingSaving, setSchedulingSaving] = useState(false);
|
|
123
|
+
const hasPersonalExternalContext = hasPersonalExternalVisibleContext({
|
|
124
|
+
boardId,
|
|
125
|
+
visibleBoardId,
|
|
126
|
+
visibleTaskSnapshot,
|
|
127
|
+
});
|
|
128
|
+
const propertyCacheBoardId =
|
|
129
|
+
hasPersonalExternalContext && visibleBoardId ? visibleBoardId : boardId;
|
|
97
130
|
|
|
98
131
|
// Helper to trigger refresh after successful mutations
|
|
99
132
|
// Note: The kanban board uses realtime subscriptions directly and doesn't
|
|
100
133
|
// register a refresh callback via the task dialog system, so calling onUpdate
|
|
101
134
|
// here won't conflict with realtime sync on the board page.
|
|
102
135
|
// Also invalidate task-history so the activity section updates immediately.
|
|
103
|
-
const triggerRefresh = useCallback(
|
|
136
|
+
const triggerRefresh = useCallback(
|
|
137
|
+
(options?: { refreshBoard?: boolean }) => {
|
|
138
|
+
const refreshBoard = options?.refreshBoard ?? true;
|
|
139
|
+
|
|
140
|
+
queryClient.invalidateQueries({ queryKey: ['task-history'] });
|
|
141
|
+
if (refreshBoard) {
|
|
142
|
+
onUpdate();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
[queryClient, onUpdate]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const patchPropertyInVisibleCaches = useCallback(
|
|
149
|
+
(patch: PropertyPatch) => {
|
|
150
|
+
if (!taskId) return;
|
|
151
|
+
|
|
152
|
+
patchTaskInVisibleCaches({
|
|
153
|
+
queryClient,
|
|
154
|
+
boardId: propertyCacheBoardId,
|
|
155
|
+
taskId,
|
|
156
|
+
updater: (task) => ({
|
|
157
|
+
...task,
|
|
158
|
+
...patch,
|
|
159
|
+
...(hasPersonalExternalContext
|
|
160
|
+
? { _localMutationAt: Date.now() }
|
|
161
|
+
: {}),
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
[hasPersonalExternalContext, propertyCacheBoardId, queryClient, taskId]
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const triggerPropertyRefresh = useCallback(() => {
|
|
104
169
|
queryClient.invalidateQueries({ queryKey: ['task-history'] });
|
|
105
|
-
|
|
106
|
-
|
|
170
|
+
if (!hasPersonalExternalContext) {
|
|
171
|
+
onUpdate();
|
|
172
|
+
}
|
|
173
|
+
}, [hasPersonalExternalContext, queryClient, onUpdate]);
|
|
107
174
|
|
|
108
175
|
const updateEstimation = useCallback(
|
|
109
176
|
async (points: number | null) => {
|
|
@@ -113,29 +180,30 @@ export function useTaskMutations({
|
|
|
113
180
|
return;
|
|
114
181
|
}
|
|
115
182
|
setEstimationSaving(true);
|
|
116
|
-
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
117
|
-
|
|
118
|
-
|
|
183
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
184
|
+
queryClient,
|
|
185
|
+
propertyCacheBoardId,
|
|
186
|
+
[taskId]
|
|
187
|
+
);
|
|
119
188
|
|
|
120
189
|
// Optimistic update - prevents flicker by updating cache immediately
|
|
121
|
-
|
|
122
|
-
queryClient,
|
|
123
|
-
boardId,
|
|
124
|
-
taskId,
|
|
125
|
-
updater: (task) => ({ ...task, estimation_points: points }),
|
|
126
|
-
});
|
|
190
|
+
patchPropertyInVisibleCaches({ estimation_points: points });
|
|
127
191
|
|
|
128
192
|
try {
|
|
129
193
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
130
194
|
estimation_points: points,
|
|
131
195
|
});
|
|
196
|
+
const updatedEstimationPoints = task.estimation_points ?? points;
|
|
197
|
+
patchPropertyInVisibleCaches({
|
|
198
|
+
estimation_points: updatedEstimationPoints,
|
|
199
|
+
});
|
|
132
200
|
broadcast?.('task:upsert', {
|
|
133
201
|
task: {
|
|
134
202
|
id: taskId,
|
|
135
|
-
estimation_points:
|
|
203
|
+
estimation_points: updatedEstimationPoints,
|
|
136
204
|
},
|
|
137
205
|
});
|
|
138
|
-
|
|
206
|
+
triggerPropertyRefresh();
|
|
139
207
|
} catch (e: any) {
|
|
140
208
|
console.error('Failed updating estimation', e);
|
|
141
209
|
// Revert optimistic update on error
|
|
@@ -155,10 +223,11 @@ export function useTaskMutations({
|
|
|
155
223
|
wsId,
|
|
156
224
|
taskId,
|
|
157
225
|
queryClient,
|
|
158
|
-
|
|
226
|
+
propertyCacheBoardId,
|
|
159
227
|
toast,
|
|
160
228
|
setEstimationPoints,
|
|
161
|
-
|
|
229
|
+
patchPropertyInVisibleCaches,
|
|
230
|
+
triggerPropertyRefresh,
|
|
162
231
|
broadcast,
|
|
163
232
|
]
|
|
164
233
|
);
|
|
@@ -170,29 +239,28 @@ export function useTaskMutations({
|
|
|
170
239
|
if (isCreateMode || !taskId || taskId === 'new') {
|
|
171
240
|
return;
|
|
172
241
|
}
|
|
173
|
-
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
174
|
-
|
|
175
|
-
|
|
242
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
243
|
+
queryClient,
|
|
244
|
+
propertyCacheBoardId,
|
|
245
|
+
[taskId]
|
|
246
|
+
);
|
|
176
247
|
|
|
177
248
|
// Optimistic update - prevents flicker
|
|
178
|
-
|
|
179
|
-
queryClient,
|
|
180
|
-
boardId,
|
|
181
|
-
taskId,
|
|
182
|
-
updater: (task) => ({ ...task, priority: newPriority }),
|
|
183
|
-
});
|
|
249
|
+
patchPropertyInVisibleCaches({ priority: newPriority });
|
|
184
250
|
|
|
185
251
|
try {
|
|
186
252
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
187
253
|
priority: newPriority,
|
|
188
254
|
});
|
|
255
|
+
const updatedPriority = task.priority ?? newPriority;
|
|
256
|
+
patchPropertyInVisibleCaches({ priority: updatedPriority });
|
|
189
257
|
broadcast?.('task:upsert', {
|
|
190
258
|
task: {
|
|
191
259
|
id: taskId,
|
|
192
|
-
priority:
|
|
260
|
+
priority: updatedPriority,
|
|
193
261
|
},
|
|
194
262
|
});
|
|
195
|
-
|
|
263
|
+
triggerPropertyRefresh();
|
|
196
264
|
} catch (e: any) {
|
|
197
265
|
console.error('Failed updating priority', e);
|
|
198
266
|
// Revert optimistic update on error
|
|
@@ -210,10 +278,11 @@ export function useTaskMutations({
|
|
|
210
278
|
wsId,
|
|
211
279
|
taskId,
|
|
212
280
|
queryClient,
|
|
213
|
-
|
|
281
|
+
propertyCacheBoardId,
|
|
214
282
|
toast,
|
|
215
283
|
setPriority,
|
|
216
|
-
|
|
284
|
+
patchPropertyInVisibleCaches,
|
|
285
|
+
triggerPropertyRefresh,
|
|
217
286
|
broadcast,
|
|
218
287
|
]
|
|
219
288
|
);
|
|
@@ -226,29 +295,30 @@ export function useTaskMutations({
|
|
|
226
295
|
}
|
|
227
296
|
|
|
228
297
|
const dateString = newDate ? newDate.toISOString() : null;
|
|
229
|
-
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
230
|
-
|
|
231
|
-
|
|
298
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
299
|
+
queryClient,
|
|
300
|
+
propertyCacheBoardId,
|
|
301
|
+
[taskId]
|
|
302
|
+
);
|
|
232
303
|
|
|
233
304
|
// Optimistic update - prevents flicker
|
|
234
|
-
|
|
235
|
-
queryClient,
|
|
236
|
-
boardId,
|
|
237
|
-
taskId,
|
|
238
|
-
updater: (task) => ({ ...task, start_date: dateString ?? undefined }),
|
|
239
|
-
});
|
|
305
|
+
patchPropertyInVisibleCaches({ start_date: dateString ?? undefined });
|
|
240
306
|
|
|
241
307
|
try {
|
|
242
308
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
243
309
|
start_date: dateString,
|
|
244
310
|
});
|
|
311
|
+
const updatedStartDate = task.start_date ?? dateString;
|
|
312
|
+
patchPropertyInVisibleCaches({
|
|
313
|
+
start_date: updatedStartDate ?? undefined,
|
|
314
|
+
});
|
|
245
315
|
broadcast?.('task:upsert', {
|
|
246
316
|
task: {
|
|
247
317
|
id: taskId,
|
|
248
|
-
start_date:
|
|
318
|
+
start_date: updatedStartDate,
|
|
249
319
|
},
|
|
250
320
|
});
|
|
251
|
-
|
|
321
|
+
triggerPropertyRefresh();
|
|
252
322
|
} catch (e: any) {
|
|
253
323
|
console.error('Failed updating start date', e);
|
|
254
324
|
// Revert optimistic update on error
|
|
@@ -265,10 +335,11 @@ export function useTaskMutations({
|
|
|
265
335
|
wsId,
|
|
266
336
|
taskId,
|
|
267
337
|
queryClient,
|
|
268
|
-
|
|
338
|
+
propertyCacheBoardId,
|
|
269
339
|
toast,
|
|
270
340
|
setStartDate,
|
|
271
|
-
|
|
341
|
+
patchPropertyInVisibleCaches,
|
|
342
|
+
triggerPropertyRefresh,
|
|
272
343
|
broadcast,
|
|
273
344
|
]
|
|
274
345
|
);
|
|
@@ -281,29 +352,28 @@ export function useTaskMutations({
|
|
|
281
352
|
}
|
|
282
353
|
|
|
283
354
|
const dateString = newDate ? newDate.toISOString() : null;
|
|
284
|
-
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
285
|
-
|
|
286
|
-
|
|
355
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
356
|
+
queryClient,
|
|
357
|
+
propertyCacheBoardId,
|
|
358
|
+
[taskId]
|
|
359
|
+
);
|
|
287
360
|
|
|
288
361
|
// Optimistic update - prevents flicker
|
|
289
|
-
|
|
290
|
-
queryClient,
|
|
291
|
-
boardId,
|
|
292
|
-
taskId,
|
|
293
|
-
updater: (task) => ({ ...task, end_date: dateString }),
|
|
294
|
-
});
|
|
362
|
+
patchPropertyInVisibleCaches({ end_date: dateString });
|
|
295
363
|
|
|
296
364
|
try {
|
|
297
365
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
298
366
|
end_date: dateString,
|
|
299
367
|
});
|
|
368
|
+
const updatedEndDate = task.end_date ?? dateString;
|
|
369
|
+
patchPropertyInVisibleCaches({ end_date: updatedEndDate });
|
|
300
370
|
broadcast?.('task:upsert', {
|
|
301
371
|
task: {
|
|
302
372
|
id: taskId,
|
|
303
|
-
end_date:
|
|
373
|
+
end_date: updatedEndDate,
|
|
304
374
|
},
|
|
305
375
|
});
|
|
306
|
-
|
|
376
|
+
triggerPropertyRefresh();
|
|
307
377
|
} catch (e: any) {
|
|
308
378
|
console.error('Failed updating end date', e);
|
|
309
379
|
// Revert optimistic update on error
|
|
@@ -320,10 +390,11 @@ export function useTaskMutations({
|
|
|
320
390
|
wsId,
|
|
321
391
|
taskId,
|
|
322
392
|
queryClient,
|
|
323
|
-
|
|
393
|
+
propertyCacheBoardId,
|
|
324
394
|
toast,
|
|
325
395
|
setEndDate,
|
|
326
|
-
|
|
396
|
+
patchPropertyInVisibleCaches,
|
|
397
|
+
triggerPropertyRefresh,
|
|
327
398
|
broadcast,
|
|
328
399
|
]
|
|
329
400
|
);
|
|
@@ -411,24 +482,23 @@ export function useTaskMutations({
|
|
|
411
482
|
|
|
412
483
|
// Optimistically update the cache instead of invalidating
|
|
413
484
|
// This prevents conflicts with realtime sync
|
|
414
|
-
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
415
|
-
taskId,
|
|
416
|
-
]);
|
|
417
|
-
patchTaskInVisibleCaches({
|
|
485
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
418
486
|
queryClient,
|
|
419
|
-
|
|
420
|
-
taskId
|
|
421
|
-
|
|
422
|
-
});
|
|
487
|
+
propertyCacheBoardId,
|
|
488
|
+
[taskId]
|
|
489
|
+
);
|
|
490
|
+
patchPropertyInVisibleCaches({ name: trimmedName });
|
|
423
491
|
|
|
424
492
|
try {
|
|
425
493
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
426
494
|
name: trimmedName,
|
|
427
495
|
});
|
|
496
|
+
const updatedName = task.name ?? trimmedName;
|
|
497
|
+
patchPropertyInVisibleCaches({ name: updatedName });
|
|
428
498
|
broadcast?.('task:upsert', {
|
|
429
|
-
task: { id: taskId, name:
|
|
499
|
+
task: { id: taskId, name: updatedName },
|
|
430
500
|
});
|
|
431
|
-
|
|
501
|
+
triggerPropertyRefresh();
|
|
432
502
|
} catch (e: any) {
|
|
433
503
|
console.error('Failed updating task name', e);
|
|
434
504
|
// Revert optimistic update without refetching visible board caches.
|
|
@@ -446,9 +516,10 @@ export function useTaskMutations({
|
|
|
446
516
|
taskId,
|
|
447
517
|
isCreateMode,
|
|
448
518
|
queryClient,
|
|
449
|
-
|
|
519
|
+
propertyCacheBoardId,
|
|
450
520
|
toast,
|
|
451
|
-
|
|
521
|
+
patchPropertyInVisibleCaches,
|
|
522
|
+
triggerPropertyRefresh,
|
|
452
523
|
broadcast,
|
|
453
524
|
]
|
|
454
525
|
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { TaskDialogActions } from './task-dialog-actions';
|
|
8
|
+
|
|
9
|
+
vi.mock('next-intl', () => ({
|
|
10
|
+
useTranslations: () => (key: string) => key,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock('@tuturuuu/ui/sonner', () => ({
|
|
14
|
+
toast: {
|
|
15
|
+
success: vi.fn(),
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe('TaskDialogActions', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('shows the optional archive action in the edit menu', () => {
|
|
25
|
+
const onArchiveTask = vi.fn();
|
|
26
|
+
const onMoreMenuOpenChange = vi.fn();
|
|
27
|
+
|
|
28
|
+
render(
|
|
29
|
+
<TaskDialogActions
|
|
30
|
+
boardId="board-1"
|
|
31
|
+
hasDraft={false}
|
|
32
|
+
isCreateMode={false}
|
|
33
|
+
moreMenuOpen
|
|
34
|
+
onArchiveTask={onArchiveTask}
|
|
35
|
+
onClearDraft={vi.fn()}
|
|
36
|
+
onClose={vi.fn()}
|
|
37
|
+
onMoreMenuOpenChange={onMoreMenuOpenChange}
|
|
38
|
+
onShowDeleteDialog={vi.fn()}
|
|
39
|
+
taskId="task-1"
|
|
40
|
+
wsId="workspace-1"
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
fireEvent.click(screen.getByText('common.archive'));
|
|
45
|
+
|
|
46
|
+
expect(onArchiveTask).toHaveBeenCalledTimes(1);
|
|
47
|
+
expect(onMoreMenuOpenChange).toHaveBeenCalledWith(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('keeps the archive action hidden when no handler is provided', () => {
|
|
51
|
+
render(
|
|
52
|
+
<TaskDialogActions
|
|
53
|
+
boardId="board-1"
|
|
54
|
+
hasDraft={false}
|
|
55
|
+
isCreateMode={false}
|
|
56
|
+
moreMenuOpen
|
|
57
|
+
onClearDraft={vi.fn()}
|
|
58
|
+
onClose={vi.fn()}
|
|
59
|
+
onMoreMenuOpenChange={vi.fn()}
|
|
60
|
+
onShowDeleteDialog={vi.fn()}
|
|
61
|
+
taskId="task-1"
|
|
62
|
+
wsId="workspace-1"
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(screen.queryByText('common.archive')).not.toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
});
|