@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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { getCurrentUserProfile } from '@tuturuuu/internal-api';
|
|
5
5
|
import { getWorkspaceTask } from '@tuturuuu/internal-api/tasks';
|
|
6
|
+
import { getUserConfig } from '@tuturuuu/internal-api/users';
|
|
6
7
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
7
8
|
import { useSearchParams } from 'next/navigation';
|
|
8
9
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
@@ -12,6 +13,10 @@ import {
|
|
|
12
13
|
WorkspacePresenceProvider,
|
|
13
14
|
} from '../providers/workspace-presence-provider';
|
|
14
15
|
import { dispatchRecentSidebarVisit } from './recent-sidebar-events';
|
|
16
|
+
import {
|
|
17
|
+
normalizeTaskDialogPresentation,
|
|
18
|
+
TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID,
|
|
19
|
+
} from './task-dialog-presentation';
|
|
15
20
|
import { TaskEditDialog } from './task-edit-dialog';
|
|
16
21
|
import {
|
|
17
22
|
dispatchTaskOpenResult,
|
|
@@ -146,19 +151,23 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
146
151
|
// Read draft mode preference from user config (same query key as useUserBooleanConfig)
|
|
147
152
|
const { data: draftModeRaw } = useQuery({
|
|
148
153
|
queryKey: ['user-config', 'TASK_DRAFT_MODE_ENABLED'],
|
|
149
|
-
queryFn: async () =>
|
|
150
|
-
|
|
151
|
-
'/api/v1/users/me/configs/TASK_DRAFT_MODE_ENABLED',
|
|
152
|
-
{ cache: 'no-store' }
|
|
153
|
-
);
|
|
154
|
-
if (!res.ok) return 'false';
|
|
155
|
-
const data = await res.json();
|
|
156
|
-
return (data.value as string) ?? 'false';
|
|
157
|
-
},
|
|
154
|
+
queryFn: async () =>
|
|
155
|
+
(await getUserConfig('TASK_DRAFT_MODE_ENABLED')).value ?? 'false',
|
|
158
156
|
staleTime: 5 * 60 * 1000,
|
|
159
157
|
});
|
|
160
158
|
const draftModeEnabled = draftModeRaw === 'true';
|
|
161
159
|
|
|
160
|
+
const { data: defaultPresentationRaw } = useQuery({
|
|
161
|
+
queryKey: ['user-config', TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID],
|
|
162
|
+
queryFn: async () =>
|
|
163
|
+
(await getUserConfig(TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID)).value ??
|
|
164
|
+
'compact',
|
|
165
|
+
staleTime: 5 * 60 * 1000,
|
|
166
|
+
});
|
|
167
|
+
const defaultPresentation = normalizeTaskDialogPresentation(
|
|
168
|
+
defaultPresentationRaw
|
|
169
|
+
);
|
|
170
|
+
|
|
162
171
|
const handleClose = () => {
|
|
163
172
|
triggerClose();
|
|
164
173
|
};
|
|
@@ -226,30 +235,12 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
226
235
|
|
|
227
236
|
const openTaskFromCurrentWorkspace = useCallback(
|
|
228
237
|
async (taskId: string) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
...init,
|
|
234
|
-
cache: 'no-store',
|
|
235
|
-
}),
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
const boardId = task.board_id;
|
|
239
|
-
if (!boardId) {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
openTask(task as Task, boardId, undefined, false, {
|
|
244
|
-
taskWsId: wsId,
|
|
245
|
-
taskWorkspacePersonal: isPersonalWorkspace,
|
|
246
|
-
});
|
|
247
|
-
return true;
|
|
248
|
-
} catch {
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
238
|
+
return openTaskById(taskId, {
|
|
239
|
+
taskWsId: wsId,
|
|
240
|
+
taskWorkspacePersonal: isPersonalWorkspace,
|
|
241
|
+
});
|
|
251
242
|
},
|
|
252
|
-
[isPersonalWorkspace,
|
|
243
|
+
[isPersonalWorkspace, openTaskById, wsId]
|
|
253
244
|
);
|
|
254
245
|
|
|
255
246
|
useEffect(() => {
|
|
@@ -269,40 +260,12 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
269
260
|
};
|
|
270
261
|
|
|
271
262
|
void (async () => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
{
|
|
279
|
-
...init,
|
|
280
|
-
cache: 'no-store',
|
|
281
|
-
}
|
|
282
|
-
),
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
const taskWithList = task as {
|
|
286
|
-
board_id?: string | null;
|
|
287
|
-
list?: {
|
|
288
|
-
board_id?: string | null;
|
|
289
|
-
} | null;
|
|
290
|
-
};
|
|
291
|
-
const boardId =
|
|
292
|
-
taskWithList.board_id || taskWithList.list?.board_id;
|
|
293
|
-
if (boardId) {
|
|
294
|
-
openTask(task as Task, boardId, undefined, false, {
|
|
295
|
-
taskWsId: requestedWsId,
|
|
296
|
-
});
|
|
297
|
-
emitOpenResult(true);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
} catch {
|
|
301
|
-
// Fall through to the generic current-user lookup below.
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const opened = await openTaskById(taskId);
|
|
263
|
+
const opened = await openTaskById(taskId, {
|
|
264
|
+
taskWsId: requestedWsId,
|
|
265
|
+
taskWorkspacePersonal: requestedWsId
|
|
266
|
+
? undefined
|
|
267
|
+
: isPersonalWorkspace,
|
|
268
|
+
});
|
|
306
269
|
emitOpenResult(opened);
|
|
307
270
|
})();
|
|
308
271
|
};
|
|
@@ -318,7 +281,7 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
318
281
|
handleTaskOpenRequest as EventListener
|
|
319
282
|
);
|
|
320
283
|
};
|
|
321
|
-
}, [
|
|
284
|
+
}, [isPersonalWorkspace, openTaskById]);
|
|
322
285
|
|
|
323
286
|
useEffect(() => {
|
|
324
287
|
const canonicalTaskId = searchParams.get('task');
|
|
@@ -348,7 +311,10 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
348
311
|
return;
|
|
349
312
|
}
|
|
350
313
|
|
|
351
|
-
void openTaskById(legacyTaskId
|
|
314
|
+
void openTaskById(legacyTaskId, {
|
|
315
|
+
taskWsId: wsId,
|
|
316
|
+
taskWorkspacePersonal: isPersonalWorkspace,
|
|
317
|
+
});
|
|
352
318
|
const nextSearchParams = new URLSearchParams(searchParams.toString());
|
|
353
319
|
nextSearchParams.delete('openTaskId');
|
|
354
320
|
|
|
@@ -358,7 +324,7 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
358
324
|
: window.location.pathname;
|
|
359
325
|
|
|
360
326
|
window.history.replaceState(window.history.state, '', nextUrl);
|
|
361
|
-
}, [openTaskById, searchParams]);
|
|
327
|
+
}, [isPersonalWorkspace, openTaskById, searchParams, wsId]);
|
|
362
328
|
|
|
363
329
|
// Open subtask creation dialog for the current task
|
|
364
330
|
const handleAddSubtask = useCallback(() => {
|
|
@@ -411,6 +377,31 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
411
377
|
const handleAddBlockedByTask = () => handleAddRelationship('blocked-by');
|
|
412
378
|
const handleAddRelatedTask = () => handleAddRelationship('related');
|
|
413
379
|
|
|
380
|
+
const handleRetryTaskLoad = useCallback(() => {
|
|
381
|
+
if (!state.task?.id) return;
|
|
382
|
+
|
|
383
|
+
void openTaskById(state.task.id, {
|
|
384
|
+
initialTask: state.task,
|
|
385
|
+
boardId: state.boardId,
|
|
386
|
+
availableLists: state.availableLists,
|
|
387
|
+
fakeTaskUrl: state.fakeTaskUrl,
|
|
388
|
+
taskWsId: state.taskWsId,
|
|
389
|
+
taskWorkspacePersonal: state.taskWorkspacePersonal,
|
|
390
|
+
taskWorkspaceTier: state.taskWorkspaceTier,
|
|
391
|
+
initialSharedContext: state.initialSharedContext,
|
|
392
|
+
});
|
|
393
|
+
}, [
|
|
394
|
+
openTaskById,
|
|
395
|
+
state.initialSharedContext,
|
|
396
|
+
state.availableLists,
|
|
397
|
+
state.boardId,
|
|
398
|
+
state.fakeTaskUrl,
|
|
399
|
+
state.task,
|
|
400
|
+
state.taskWorkspacePersonal,
|
|
401
|
+
state.taskWorkspaceTier,
|
|
402
|
+
state.taskWsId,
|
|
403
|
+
]);
|
|
404
|
+
|
|
414
405
|
// Track presence location when the dialog is open in edit mode.
|
|
415
406
|
// On kanban boards, BoardUserPresenceAvatarsComponent also calls updateLocation
|
|
416
407
|
// with the same args — this is idempotent (same location = no-op).
|
|
@@ -419,7 +410,14 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
419
410
|
const wsUpdateLocation = wsPresence?.updateLocation;
|
|
420
411
|
|
|
421
412
|
useEffect(() => {
|
|
422
|
-
if (
|
|
413
|
+
if (
|
|
414
|
+
!wsUpdateLocation ||
|
|
415
|
+
!state.isOpen ||
|
|
416
|
+
state.mode === 'create' ||
|
|
417
|
+
state.isHydratingTask ||
|
|
418
|
+
state.taskLoadError
|
|
419
|
+
)
|
|
420
|
+
return;
|
|
423
421
|
const taskId = state.task?.id;
|
|
424
422
|
const boardId = state.boardId;
|
|
425
423
|
if (!taskId || !boardId) return;
|
|
@@ -433,6 +431,8 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
433
431
|
wsUpdateLocation,
|
|
434
432
|
state.isOpen,
|
|
435
433
|
state.mode,
|
|
434
|
+
state.isHydratingTask,
|
|
435
|
+
state.taskLoadError,
|
|
436
436
|
state.task?.id,
|
|
437
437
|
state.boardId,
|
|
438
438
|
]);
|
|
@@ -441,7 +441,9 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
441
441
|
if (
|
|
442
442
|
typeof window === 'undefined' ||
|
|
443
443
|
!state.isOpen ||
|
|
444
|
-
state.mode === 'create'
|
|
444
|
+
state.mode === 'create' ||
|
|
445
|
+
state.isHydratingTask ||
|
|
446
|
+
state.taskLoadError
|
|
445
447
|
) {
|
|
446
448
|
return;
|
|
447
449
|
}
|
|
@@ -493,19 +495,24 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
493
495
|
}, [
|
|
494
496
|
state.boardId,
|
|
495
497
|
state.isOpen,
|
|
498
|
+
state.isHydratingTask,
|
|
496
499
|
state.mode,
|
|
497
500
|
state.task,
|
|
498
501
|
state.taskWsId,
|
|
499
502
|
state.taskWorkspacePersonal,
|
|
503
|
+
state.taskLoadError,
|
|
500
504
|
isPersonalWorkspace,
|
|
501
505
|
wsId,
|
|
502
506
|
]);
|
|
503
507
|
|
|
504
|
-
// Determine if the task needs its own presence provider (cross-workspace tasks)
|
|
508
|
+
// Determine if the task needs its own presence provider (cross-workspace tasks).
|
|
509
|
+
// Keep the provider shell mounted from the initial snapshot when taskWsId is
|
|
510
|
+
// already known, otherwise hydration can wrap the open dialog in a new
|
|
511
|
+
// provider and Radix replays the compact dialog entrance animation.
|
|
505
512
|
const needsOwnProvider =
|
|
506
|
-
state.realtimeEnabled
|
|
507
|
-
|
|
508
|
-
|
|
513
|
+
state.taskWsId && (!wsPresence?.realtimeEnabled || state.taskWsId !== wsId);
|
|
514
|
+
const ownProviderEnabled =
|
|
515
|
+
!!state.realtimeEnabled && !state.taskWorkspacePersonal;
|
|
509
516
|
|
|
510
517
|
if (!state.isOpen || !state.task) {
|
|
511
518
|
return null;
|
|
@@ -519,16 +526,25 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
519
526
|
boardId={state.boardId || ''}
|
|
520
527
|
isOpen={state.isOpen}
|
|
521
528
|
availableLists={state.availableLists}
|
|
529
|
+
sharedContext={
|
|
530
|
+
state.isHydratingTask || state.taskLoadError
|
|
531
|
+
? state.initialSharedContext
|
|
532
|
+
: undefined
|
|
533
|
+
}
|
|
522
534
|
filters={state.filters}
|
|
523
535
|
mode={state.mode}
|
|
524
536
|
collaborationMode={state.collaborationMode}
|
|
525
537
|
realtimeEnabled={state.realtimeEnabled}
|
|
538
|
+
isHydratingTask={state.isHydratingTask}
|
|
539
|
+
taskLoadError={state.taskLoadError}
|
|
540
|
+
taskHydrationVersion={state.taskHydrationVersion}
|
|
526
541
|
isPersonalWorkspace={isPersonalWorkspace}
|
|
527
542
|
parentTaskId={state.parentTaskId}
|
|
528
543
|
parentTaskName={state.parentTaskName}
|
|
529
544
|
pendingRelationship={state.pendingRelationship}
|
|
530
545
|
currentUser={currentUser || undefined}
|
|
531
546
|
draftModeEnabled={draftModeEnabled}
|
|
547
|
+
defaultPresentation={defaultPresentation}
|
|
532
548
|
draftId={state.draftId}
|
|
533
549
|
onClose={handleClose}
|
|
534
550
|
onUpdate={triggerUpdate}
|
|
@@ -538,6 +554,7 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
538
554
|
onAddBlockingTask={handleAddBlockingTask}
|
|
539
555
|
onAddBlockedByTask={handleAddBlockedByTask}
|
|
540
556
|
onAddRelatedTask={handleAddRelatedTask}
|
|
557
|
+
onRetryTaskLoad={handleRetryTaskLoad}
|
|
541
558
|
/>
|
|
542
559
|
);
|
|
543
560
|
|
|
@@ -546,7 +563,7 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
546
563
|
<WorkspacePresenceProvider
|
|
547
564
|
wsId={state.taskWsId}
|
|
548
565
|
tier={state.taskWorkspaceTier ?? null}
|
|
549
|
-
enabled={
|
|
566
|
+
enabled={ownProviderEnabled}
|
|
550
567
|
>
|
|
551
568
|
{dialog}
|
|
552
569
|
</WorkspacePresenceProvider>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID =
|
|
2
|
+
'TASK_DIALOG_DEFAULT_PRESENTATION';
|
|
3
|
+
|
|
4
|
+
export type TaskDialogPresentation = 'compact' | 'fullscreen';
|
|
5
|
+
|
|
6
|
+
export function normalizeTaskDialogPresentation(
|
|
7
|
+
value: unknown,
|
|
8
|
+
fallback: TaskDialogPresentation = 'compact'
|
|
9
|
+
): TaskDialogPresentation {
|
|
10
|
+
return value === 'fullscreen' || value === 'compact' ? value : fallback;
|
|
11
|
+
}
|
|
@@ -64,7 +64,7 @@ describe('CompactTaskCreatePopover', () => {
|
|
|
64
64
|
it('renders compact create content with accessible icon actions', () => {
|
|
65
65
|
renderCompactTaskCreatePopover();
|
|
66
66
|
|
|
67
|
-
expect(screen.getByTestId('compact-task-
|
|
67
|
+
expect(screen.getByTestId('compact-task-dialog-panel')).toBeTruthy();
|
|
68
68
|
expect(screen.getByText('Create task')).toBeTruthy();
|
|
69
69
|
expect(screen.getByLabelText('Task title')).toHaveProperty(
|
|
70
70
|
'value',
|
|
@@ -138,4 +138,131 @@ describe('CompactTaskCreatePopover', () => {
|
|
|
138
138
|
screen.getAllByRole('button', { name: 'task-drafts.save_as_draft' })
|
|
139
139
|
).toHaveLength(2);
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
it('renders compact edit content without create-only footer actions', () => {
|
|
143
|
+
render(
|
|
144
|
+
<Dialog open={true}>
|
|
145
|
+
<CompactTaskCreatePopover
|
|
146
|
+
title="Edit task"
|
|
147
|
+
titleInput={<input aria-label="Task title" defaultValue="Existing" />}
|
|
148
|
+
propertyControls={
|
|
149
|
+
<button type="button" aria-label="List: Inbox">
|
|
150
|
+
List
|
|
151
|
+
</button>
|
|
152
|
+
}
|
|
153
|
+
smartAction={
|
|
154
|
+
<button type="button" aria-label="Smart action">
|
|
155
|
+
Smart
|
|
156
|
+
</button>
|
|
157
|
+
}
|
|
158
|
+
onClose={vi.fn()}
|
|
159
|
+
onFullscreen={vi.fn()}
|
|
160
|
+
/>
|
|
161
|
+
</Dialog>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(screen.getByText('Edit task')).toBeTruthy();
|
|
165
|
+
expect(screen.getByLabelText('Task title')).toHaveProperty(
|
|
166
|
+
'value',
|
|
167
|
+
'Existing'
|
|
168
|
+
);
|
|
169
|
+
expect(screen.getByLabelText('List: Inbox')).toBeTruthy();
|
|
170
|
+
expect(screen.getByLabelText('Smart action')).toBeTruthy();
|
|
171
|
+
expect(
|
|
172
|
+
screen.queryByLabelText('task-drafts.save_as_draft')
|
|
173
|
+
).not.toBeInTheDocument();
|
|
174
|
+
expect(
|
|
175
|
+
screen.queryByRole('button', {
|
|
176
|
+
name: 'ws-task-boards.dialog.create_task',
|
|
177
|
+
})
|
|
178
|
+
).not.toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('renders compact edit actions when provided', () => {
|
|
182
|
+
const onDelete = vi.fn();
|
|
183
|
+
const onDone = vi.fn();
|
|
184
|
+
const onClosed = vi.fn();
|
|
185
|
+
|
|
186
|
+
render(
|
|
187
|
+
<Dialog open={true}>
|
|
188
|
+
<CompactTaskCreatePopover
|
|
189
|
+
title="Edit task"
|
|
190
|
+
titleInput={<input aria-label="Task title" defaultValue="Existing" />}
|
|
191
|
+
propertyControls={
|
|
192
|
+
<button type="button" aria-label="List: Inbox">
|
|
193
|
+
List
|
|
194
|
+
</button>
|
|
195
|
+
}
|
|
196
|
+
editActions={
|
|
197
|
+
<>
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
aria-label="common.mark_as_done"
|
|
201
|
+
onClick={onDone}
|
|
202
|
+
>
|
|
203
|
+
Done
|
|
204
|
+
</button>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
aria-label="common.archive"
|
|
208
|
+
onClick={onClosed}
|
|
209
|
+
>
|
|
210
|
+
Archive
|
|
211
|
+
</button>
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
aria-label="common.delete_task"
|
|
215
|
+
onClick={onDelete}
|
|
216
|
+
>
|
|
217
|
+
Delete
|
|
218
|
+
</button>
|
|
219
|
+
</>
|
|
220
|
+
}
|
|
221
|
+
onClose={vi.fn()}
|
|
222
|
+
onFullscreen={vi.fn()}
|
|
223
|
+
/>
|
|
224
|
+
</Dialog>
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
fireEvent.click(screen.getByLabelText('common.mark_as_done'));
|
|
228
|
+
fireEvent.click(screen.getByLabelText('common.archive'));
|
|
229
|
+
fireEvent.click(screen.getByLabelText('common.delete_task'));
|
|
230
|
+
|
|
231
|
+
expect(
|
|
232
|
+
screen.getByLabelText('common.delete_task').closest('.border-b')
|
|
233
|
+
).toBeTruthy();
|
|
234
|
+
expect(
|
|
235
|
+
screen.getByLabelText('common.delete_task').closest('.border-t')
|
|
236
|
+
).toBeNull();
|
|
237
|
+
expect(onDone).toHaveBeenCalledTimes(1);
|
|
238
|
+
expect(onClosed).toHaveBeenCalledTimes(1);
|
|
239
|
+
expect(onDelete).toHaveBeenCalledTimes(1);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('keeps the compact edit title accessible but not visibly rendered', () => {
|
|
243
|
+
render(
|
|
244
|
+
<Dialog open={true}>
|
|
245
|
+
<CompactTaskCreatePopover
|
|
246
|
+
title="Edit task"
|
|
247
|
+
showHeaderTitle={false}
|
|
248
|
+
titleInput={<input aria-label="Task title" defaultValue="Existing" />}
|
|
249
|
+
propertyControls={
|
|
250
|
+
<button type="button" aria-label="List: Inbox">
|
|
251
|
+
List
|
|
252
|
+
</button>
|
|
253
|
+
}
|
|
254
|
+
editActions={
|
|
255
|
+
<button type="button" aria-label="common.delete_task">
|
|
256
|
+
Delete
|
|
257
|
+
</button>
|
|
258
|
+
}
|
|
259
|
+
onClose={vi.fn()}
|
|
260
|
+
onFullscreen={vi.fn()}
|
|
261
|
+
/>
|
|
262
|
+
</Dialog>
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(screen.getByText('Edit task')).toHaveClass('sr-only');
|
|
266
|
+
expect(screen.getByLabelText('common.delete_task')).toBeTruthy();
|
|
267
|
+
});
|
|
141
268
|
});
|
package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx
CHANGED
|
@@ -17,24 +17,29 @@ import { useTranslations } from 'next-intl';
|
|
|
17
17
|
import type { ReactNode } from 'react';
|
|
18
18
|
import { QuickSettingsPopover } from './quick-settings-popover';
|
|
19
19
|
|
|
20
|
-
interface
|
|
20
|
+
interface CompactTaskDialogPanelProps {
|
|
21
21
|
title: string;
|
|
22
22
|
description?: ReactNode;
|
|
23
23
|
icon?: ReactNode;
|
|
24
24
|
iconBgClass?: string;
|
|
25
25
|
iconRingClass?: string;
|
|
26
26
|
titleInput: ReactNode;
|
|
27
|
+
showHeaderTitle?: boolean;
|
|
28
|
+
taskStatus?: ReactNode;
|
|
27
29
|
propertyControls: ReactNode;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
editActions?: ReactNode;
|
|
31
|
+
smartAction?: ReactNode;
|
|
32
|
+
smartPanel?: ReactNode;
|
|
33
|
+
saveAsDraft?: boolean;
|
|
34
|
+
createMultiple?: boolean;
|
|
35
|
+
canSave?: boolean;
|
|
36
|
+
isLoading?: boolean;
|
|
32
37
|
isPersonalWorkspace?: boolean;
|
|
33
|
-
onSaveAsDraftChange
|
|
34
|
-
onCreateMultipleChange
|
|
38
|
+
onSaveAsDraftChange?: (value: boolean) => void;
|
|
39
|
+
onCreateMultipleChange?: (value: boolean) => void;
|
|
35
40
|
onClose: () => void;
|
|
36
41
|
onFullscreen: () => void;
|
|
37
|
-
onSave
|
|
42
|
+
onSave?: () => void;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
function CompactIconButton({
|
|
@@ -71,58 +76,82 @@ function CompactIconButton({
|
|
|
71
76
|
);
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
export function
|
|
79
|
+
export function CompactTaskDialogPanel({
|
|
75
80
|
title,
|
|
76
81
|
description,
|
|
77
82
|
icon,
|
|
78
83
|
iconBgClass = 'bg-dynamic-orange/10',
|
|
79
84
|
iconRingClass = 'ring-dynamic-orange/20',
|
|
80
85
|
titleInput,
|
|
86
|
+
showHeaderTitle = true,
|
|
87
|
+
taskStatus,
|
|
81
88
|
propertyControls,
|
|
89
|
+
editActions,
|
|
90
|
+
smartAction,
|
|
91
|
+
smartPanel,
|
|
82
92
|
saveAsDraft,
|
|
83
93
|
createMultiple,
|
|
84
94
|
canSave,
|
|
85
|
-
isLoading,
|
|
95
|
+
isLoading = false,
|
|
86
96
|
isPersonalWorkspace,
|
|
87
97
|
onSaveAsDraftChange,
|
|
88
98
|
onCreateMultipleChange,
|
|
89
99
|
onClose,
|
|
90
100
|
onFullscreen,
|
|
91
101
|
onSave,
|
|
92
|
-
}:
|
|
102
|
+
}: CompactTaskDialogPanelProps) {
|
|
93
103
|
const t = useTranslations();
|
|
104
|
+
const hasCreateActions =
|
|
105
|
+
typeof saveAsDraft === 'boolean' &&
|
|
106
|
+
typeof createMultiple === 'boolean' &&
|
|
107
|
+
typeof canSave === 'boolean' &&
|
|
108
|
+
!!onSave &&
|
|
109
|
+
!!onSaveAsDraftChange &&
|
|
110
|
+
!!onCreateMultipleChange;
|
|
94
111
|
const saveLabel = saveAsDraft
|
|
95
112
|
? t('task-drafts.save_as_draft')
|
|
96
113
|
: t('ws-task-boards.dialog.create_task');
|
|
114
|
+
const hasHeaderTitle = showHeaderTitle;
|
|
97
115
|
|
|
98
116
|
return (
|
|
99
117
|
<div
|
|
100
|
-
data-testid="compact-task-
|
|
118
|
+
data-testid="compact-task-dialog-panel"
|
|
101
119
|
className="flex max-h-[calc(100vh-2rem)] min-h-0 flex-col overflow-hidden rounded-lg bg-background"
|
|
102
120
|
>
|
|
103
|
-
<div
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
<div
|
|
122
|
+
className={cn(
|
|
123
|
+
'flex items-start gap-3 border-b px-4 py-3',
|
|
124
|
+
hasHeaderTitle ? 'justify-between' : 'justify-end'
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
{hasHeaderTitle ? (
|
|
128
|
+
<div className="flex min-w-0 items-start gap-2.5">
|
|
129
|
+
<div
|
|
130
|
+
className={cn(
|
|
131
|
+
'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ring-1',
|
|
132
|
+
iconBgClass,
|
|
133
|
+
iconRingClass
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
{icon ?? <ListTodo className="h-4 w-4 text-dynamic-orange" />}
|
|
137
|
+
</div>
|
|
138
|
+
<div className="min-w-0 space-y-0.5">
|
|
139
|
+
<DialogTitle className="truncate font-semibold text-base">
|
|
140
|
+
{title}
|
|
141
|
+
</DialogTitle>
|
|
142
|
+
{description && (
|
|
143
|
+
<DialogDescription className="truncate text-muted-foreground text-xs">
|
|
144
|
+
{description}
|
|
145
|
+
</DialogDescription>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
123
148
|
</div>
|
|
124
|
-
|
|
149
|
+
) : (
|
|
150
|
+
<DialogTitle className="sr-only">{title}</DialogTitle>
|
|
151
|
+
)}
|
|
125
152
|
<div className="flex shrink-0 items-center gap-1">
|
|
153
|
+
{smartAction}
|
|
154
|
+
{editActions}
|
|
126
155
|
<Tooltip>
|
|
127
156
|
<TooltipTrigger asChild>
|
|
128
157
|
<Button
|
|
@@ -160,49 +189,55 @@ export function CompactTaskCreatePopover({
|
|
|
160
189
|
|
|
161
190
|
<div className="min-h-0 space-y-3 overflow-y-auto px-4 py-3">
|
|
162
191
|
{titleInput}
|
|
192
|
+
{taskStatus}
|
|
163
193
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
164
194
|
{propertyControls}
|
|
165
195
|
</div>
|
|
196
|
+
{smartPanel}
|
|
166
197
|
</div>
|
|
167
198
|
|
|
168
|
-
|
|
169
|
-
<div className="flex items-center gap-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
199
|
+
{hasCreateActions && (
|
|
200
|
+
<div className="flex items-center justify-between gap-2 border-t bg-muted/20 px-4 py-3">
|
|
201
|
+
<div className="flex items-center gap-1">
|
|
202
|
+
<CompactIconButton
|
|
203
|
+
active={!!saveAsDraft}
|
|
204
|
+
label={t('task-drafts.save_as_draft')}
|
|
205
|
+
onClick={() => onSaveAsDraftChange?.(!saveAsDraft)}
|
|
206
|
+
>
|
|
207
|
+
<FileEdit className="h-4 w-4" />
|
|
208
|
+
</CompactIconButton>
|
|
209
|
+
<CompactIconButton
|
|
210
|
+
active={!!createMultiple}
|
|
211
|
+
label={t('ws-task-boards.dialog.create_multiple')}
|
|
212
|
+
onClick={() => onCreateMultipleChange?.(!createMultiple)}
|
|
213
|
+
>
|
|
214
|
+
<Copy className="h-4 w-4" />
|
|
215
|
+
</CompactIconButton>
|
|
216
|
+
<QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
|
|
217
|
+
</div>
|
|
218
|
+
<Button
|
|
219
|
+
type="button"
|
|
220
|
+
size="sm"
|
|
221
|
+
disabled={!canSave}
|
|
222
|
+
onClick={() => onSave?.()}
|
|
223
|
+
className="min-w-28"
|
|
181
224
|
>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
225
|
+
{isLoading ? (
|
|
226
|
+
<>
|
|
227
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
228
|
+
{t('ws-task-boards.dialog.saving')}
|
|
229
|
+
</>
|
|
230
|
+
) : (
|
|
231
|
+
<>
|
|
232
|
+
<Check className="h-4 w-4" />
|
|
233
|
+
{saveLabel}
|
|
234
|
+
</>
|
|
235
|
+
)}
|
|
236
|
+
</Button>
|
|
185
237
|
</div>
|
|
186
|
-
|
|
187
|
-
type="button"
|
|
188
|
-
size="sm"
|
|
189
|
-
disabled={!canSave}
|
|
190
|
-
onClick={onSave}
|
|
191
|
-
className="min-w-28"
|
|
192
|
-
>
|
|
193
|
-
{isLoading ? (
|
|
194
|
-
<>
|
|
195
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
196
|
-
{t('ws-task-boards.dialog.saving')}
|
|
197
|
-
</>
|
|
198
|
-
) : (
|
|
199
|
-
<>
|
|
200
|
-
<Check className="h-4 w-4" />
|
|
201
|
-
{saveLabel}
|
|
202
|
-
</>
|
|
203
|
-
)}
|
|
204
|
-
</Button>
|
|
205
|
-
</div>
|
|
238
|
+
)}
|
|
206
239
|
</div>
|
|
207
240
|
);
|
|
208
241
|
}
|
|
242
|
+
|
|
243
|
+
export const CompactTaskCreatePopover = CompactTaskDialogPanel;
|