@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
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
useTaskDialogContext,
|
|
10
10
|
} from '../task-dialog-provider';
|
|
11
11
|
|
|
12
|
-
const {
|
|
13
|
-
|
|
12
|
+
const { mockGetTaskDialogHydration } = vi.hoisted(() => ({
|
|
13
|
+
mockGetTaskDialogHydration: vi.fn(),
|
|
14
14
|
}));
|
|
15
15
|
|
|
16
16
|
vi.mock('@tuturuuu/internal-api/tasks', async () => {
|
|
@@ -20,7 +20,7 @@ vi.mock('@tuturuuu/internal-api/tasks', async () => {
|
|
|
20
20
|
|
|
21
21
|
return {
|
|
22
22
|
...actual,
|
|
23
|
-
|
|
23
|
+
getTaskDialogHydration: mockGetTaskDialogHydration,
|
|
24
24
|
};
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -59,13 +59,25 @@ const wrapper = ({ children }: { children: ReactNode }) => (
|
|
|
59
59
|
<TaskDialogProvider>{children}</TaskDialogProvider>
|
|
60
60
|
);
|
|
61
61
|
|
|
62
|
+
function createDeferred<T>() {
|
|
63
|
+
let resolve!: (value: T) => void;
|
|
64
|
+
let reject!: (reason?: unknown) => void;
|
|
65
|
+
const promise = new Promise<T>((promiseResolve, promiseReject) => {
|
|
66
|
+
resolve = promiseResolve;
|
|
67
|
+
reject = promiseReject;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return { promise, resolve, reject };
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
describe('TaskDialogProvider', () => {
|
|
63
74
|
afterEach(() => {
|
|
75
|
+
mockGetTaskDialogHydration.mockReset();
|
|
64
76
|
vi.useRealTimers();
|
|
65
77
|
});
|
|
66
78
|
|
|
67
79
|
it('should enable collaborationMode for paid task workspaces opened by id', async () => {
|
|
68
|
-
|
|
80
|
+
mockGetTaskDialogHydration.mockResolvedValueOnce({
|
|
69
81
|
task: {
|
|
70
82
|
...mockTask,
|
|
71
83
|
list: { board_id: 'board-1' },
|
|
@@ -87,7 +99,7 @@ describe('TaskDialogProvider', () => {
|
|
|
87
99
|
});
|
|
88
100
|
|
|
89
101
|
it('should disable collaborationMode for free task workspaces opened by id', async () => {
|
|
90
|
-
|
|
102
|
+
mockGetTaskDialogHydration.mockResolvedValueOnce({
|
|
91
103
|
task: {
|
|
92
104
|
...mockTask,
|
|
93
105
|
list: { board_id: 'board-1' },
|
|
@@ -108,6 +120,206 @@ describe('TaskDialogProvider', () => {
|
|
|
108
120
|
expect(result.current.state.taskWorkspaceTier).toBe('FREE');
|
|
109
121
|
});
|
|
110
122
|
|
|
123
|
+
it('opens by id immediately from an initial snapshot before hydrating task details', async () => {
|
|
124
|
+
const initialSharedContext = {
|
|
125
|
+
boardConfig: {
|
|
126
|
+
id: 'board-1',
|
|
127
|
+
name: 'Visible board',
|
|
128
|
+
ws_id: 'workspace-1',
|
|
129
|
+
ticket_prefix: 'VIS',
|
|
130
|
+
},
|
|
131
|
+
availableLists: [mockList],
|
|
132
|
+
workspaceLabels: [],
|
|
133
|
+
workspaceMembers: [],
|
|
134
|
+
workspaceProjects: [],
|
|
135
|
+
};
|
|
136
|
+
const deferred = createDeferred<{
|
|
137
|
+
task: Task & { list?: { board_id?: string | null } | null };
|
|
138
|
+
availableLists: TaskList[];
|
|
139
|
+
taskWsId: string;
|
|
140
|
+
taskWorkspacePersonal: boolean;
|
|
141
|
+
taskWorkspaceTier: 'PRO';
|
|
142
|
+
}>();
|
|
143
|
+
mockGetTaskDialogHydration.mockReturnValueOnce(deferred.promise);
|
|
144
|
+
|
|
145
|
+
const { result } = renderHook(() => useTaskDialogContext(), { wrapper });
|
|
146
|
+
let openPromise!: Promise<boolean>;
|
|
147
|
+
|
|
148
|
+
act(() => {
|
|
149
|
+
openPromise = result.current.openTaskById(mockTask.id, {
|
|
150
|
+
initialTask: { ...mockTask, name: 'Visible snapshot' },
|
|
151
|
+
boardId: 'board-1',
|
|
152
|
+
availableLists: [mockList],
|
|
153
|
+
taskWsId: 'workspace-1',
|
|
154
|
+
initialSharedContext,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.current.state).toMatchObject({
|
|
159
|
+
isOpen: true,
|
|
160
|
+
isHydratingTask: true,
|
|
161
|
+
taskLoadError: false,
|
|
162
|
+
boardId: 'board-1',
|
|
163
|
+
realtimeEnabled: false,
|
|
164
|
+
initialSharedContext,
|
|
165
|
+
task: {
|
|
166
|
+
id: mockTask.id,
|
|
167
|
+
name: 'Visible snapshot',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await act(async () => {
|
|
172
|
+
deferred.resolve({
|
|
173
|
+
task: {
|
|
174
|
+
...mockTask,
|
|
175
|
+
name: 'Hydrated task',
|
|
176
|
+
list: { board_id: 'board-1' },
|
|
177
|
+
},
|
|
178
|
+
availableLists: [mockList],
|
|
179
|
+
taskWsId: 'workspace-1',
|
|
180
|
+
taskWorkspacePersonal: false,
|
|
181
|
+
taskWorkspaceTier: 'PRO',
|
|
182
|
+
});
|
|
183
|
+
await openPromise;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(mockGetTaskDialogHydration).toHaveBeenCalledWith(
|
|
187
|
+
mockTask.id,
|
|
188
|
+
{
|
|
189
|
+
taskWsId: 'workspace-1',
|
|
190
|
+
taskWorkspacePersonal: undefined,
|
|
191
|
+
taskWorkspaceTier: undefined,
|
|
192
|
+
},
|
|
193
|
+
expect.objectContaining({
|
|
194
|
+
fetch: expect.any(Function),
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
expect(result.current.state).toMatchObject({
|
|
198
|
+
isOpen: true,
|
|
199
|
+
isHydratingTask: false,
|
|
200
|
+
taskLoadError: false,
|
|
201
|
+
collaborationMode: true,
|
|
202
|
+
realtimeEnabled: true,
|
|
203
|
+
taskHydrationVersion: 1,
|
|
204
|
+
taskWorkspaceTier: 'PRO',
|
|
205
|
+
task: {
|
|
206
|
+
id: mockTask.id,
|
|
207
|
+
name: 'Hydrated task',
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
expect(result.current.state.initialSharedContext).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('keeps the dialog open in a non-editable error state when hydration fails', async () => {
|
|
214
|
+
const initialSharedContext = {
|
|
215
|
+
boardConfig: {
|
|
216
|
+
id: 'board-1',
|
|
217
|
+
name: 'Visible board',
|
|
218
|
+
ws_id: 'workspace-1',
|
|
219
|
+
ticket_prefix: 'VIS',
|
|
220
|
+
},
|
|
221
|
+
availableLists: [mockList],
|
|
222
|
+
workspaceLabels: [],
|
|
223
|
+
workspaceMembers: [],
|
|
224
|
+
workspaceProjects: [],
|
|
225
|
+
};
|
|
226
|
+
const deferred = createDeferred<never>();
|
|
227
|
+
mockGetTaskDialogHydration.mockReturnValueOnce(deferred.promise);
|
|
228
|
+
|
|
229
|
+
const { result } = renderHook(() => useTaskDialogContext(), { wrapper });
|
|
230
|
+
let openPromise!: Promise<boolean>;
|
|
231
|
+
|
|
232
|
+
act(() => {
|
|
233
|
+
openPromise = result.current.openTaskById(mockTask.id, {
|
|
234
|
+
initialTask: { ...mockTask, name: 'Visible snapshot' },
|
|
235
|
+
boardId: 'board-1',
|
|
236
|
+
initialSharedContext,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(result.current.state.isOpen).toBe(true);
|
|
241
|
+
expect(result.current.state.isHydratingTask).toBe(true);
|
|
242
|
+
|
|
243
|
+
await act(async () => {
|
|
244
|
+
deferred.reject(new Error('network failed'));
|
|
245
|
+
await openPromise;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(result.current.state).toMatchObject({
|
|
249
|
+
isOpen: true,
|
|
250
|
+
isHydratingTask: false,
|
|
251
|
+
taskLoadError: true,
|
|
252
|
+
initialSharedContext,
|
|
253
|
+
task: {
|
|
254
|
+
id: mockTask.id,
|
|
255
|
+
name: 'Visible snapshot',
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('ignores stale hydration responses after another task opens', async () => {
|
|
261
|
+
const firstDeferred = createDeferred<{
|
|
262
|
+
task: Task & { list?: { board_id?: string | null } | null };
|
|
263
|
+
availableLists: TaskList[];
|
|
264
|
+
taskWsId: string;
|
|
265
|
+
taskWorkspacePersonal: boolean;
|
|
266
|
+
taskWorkspaceTier: 'PRO';
|
|
267
|
+
}>();
|
|
268
|
+
mockGetTaskDialogHydration.mockReturnValueOnce(firstDeferred.promise);
|
|
269
|
+
|
|
270
|
+
const { result } = renderHook(() => useTaskDialogContext(), { wrapper });
|
|
271
|
+
let firstOpenPromise!: Promise<boolean>;
|
|
272
|
+
|
|
273
|
+
act(() => {
|
|
274
|
+
firstOpenPromise = result.current.openTaskById('task-1', {
|
|
275
|
+
initialTask: { ...mockTask, id: 'task-1', name: 'First snapshot' },
|
|
276
|
+
boardId: 'board-1',
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
act(() => {
|
|
280
|
+
result.current.closeDialog();
|
|
281
|
+
});
|
|
282
|
+
act(() => {
|
|
283
|
+
result.current.openTask(
|
|
284
|
+
{ ...mockTask, id: 'task-2', name: 'Second task' },
|
|
285
|
+
'board-1',
|
|
286
|
+
[mockList]
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(result.current.state).toMatchObject({
|
|
291
|
+
isOpen: true,
|
|
292
|
+
task: {
|
|
293
|
+
id: 'task-2',
|
|
294
|
+
name: 'Second task',
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await act(async () => {
|
|
299
|
+
firstDeferred.resolve({
|
|
300
|
+
task: {
|
|
301
|
+
...mockTask,
|
|
302
|
+
id: 'task-1',
|
|
303
|
+
name: 'Stale hydrated task',
|
|
304
|
+
list: { board_id: 'board-1' },
|
|
305
|
+
},
|
|
306
|
+
availableLists: [mockList],
|
|
307
|
+
taskWsId: 'workspace-1',
|
|
308
|
+
taskWorkspacePersonal: false,
|
|
309
|
+
taskWorkspaceTier: 'PRO',
|
|
310
|
+
});
|
|
311
|
+
await firstOpenPromise;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(result.current.state).toMatchObject({
|
|
315
|
+
isOpen: true,
|
|
316
|
+
task: {
|
|
317
|
+
id: 'task-2',
|
|
318
|
+
name: 'Second task',
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
111
323
|
it('should provide initial dialog state', () => {
|
|
112
324
|
const { result } = renderHook(() => useTaskDialogContext(), { wrapper });
|
|
113
325
|
|
|
@@ -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(
|