@tuturuuu/ui 0.9.0 → 0.10.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 +6 -5
- package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
- package/src/components/ui/custom/nav-link.test.tsx +165 -0
- package/src/components/ui/custom/nav-link.tsx +69 -11
- package/src/components/ui/custom/navigation.tsx +1 -0
- package/src/components/ui/custom/settings/task-settings.tsx +104 -0
- package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
- package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
- package/src/components/ui/custom/settings-dialog-search.ts +75 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
- package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
- package/src/components/ui/custom/workspace-select.tsx +17 -16
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +16 -0
- package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
- package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
- package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
- package/src/components/ui/tu-do/boards/form.tsx +1 -1
- package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
- package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
- package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
- package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
- package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
- package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +116 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +21 -0
- package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
- package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +14 -4
- package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
- package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
- package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
- package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
- package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
- package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +9 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
- package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
- package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
- package/src/hooks/useBoardPresence.ts +364 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
- package/src/lib/workspace-actions.ts +2 -6
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
DropdownMenuTrigger,
|
|
31
31
|
} from '@tuturuuu/ui/dropdown-menu';
|
|
32
32
|
import { cn } from '@tuturuuu/utils/format';
|
|
33
|
+
import type React from 'react';
|
|
33
34
|
import { useMemo, useState } from 'react';
|
|
34
35
|
import { TaskCard } from '../../task';
|
|
35
36
|
import type { KanbanDeadlineSections } from './kanban-deadline-tasks';
|
|
@@ -64,6 +65,8 @@ interface KanbanDeadlinePanelsProps {
|
|
|
64
65
|
boardId: string;
|
|
65
66
|
bulkUpdateCustomDueDate: (date: Date | null) => Promise<void>;
|
|
66
67
|
isPersonalWorkspace: boolean;
|
|
68
|
+
canUseBoardAssignees?: boolean;
|
|
69
|
+
assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
|
|
67
70
|
labels: KanbanDeadlineLabels;
|
|
68
71
|
onClearSelection: () => void;
|
|
69
72
|
onSectionCollapsedChange?: (
|
|
@@ -78,8 +81,10 @@ interface KanbanDeadlinePanelsProps {
|
|
|
78
81
|
onUpdate: () => void;
|
|
79
82
|
optimisticUpdateInProgress: Set<string>;
|
|
80
83
|
sections: KanbanDeadlineSections;
|
|
84
|
+
loading?: boolean;
|
|
81
85
|
collapsedSections?: KanbanDeadlineCollapsedState;
|
|
82
86
|
pinnedSections?: KanbanDeadlineCollapsedState;
|
|
87
|
+
stickyOffsets?: Partial<Record<KanbanDeadlineSection, string>>;
|
|
83
88
|
deadlineNow?: number;
|
|
84
89
|
selectedTasks: Set<string>;
|
|
85
90
|
taskLists: TaskList[];
|
|
@@ -216,6 +221,34 @@ function getTaskListForDeadlineTask(task: Task, lists: TaskList[]) {
|
|
|
216
221
|
);
|
|
217
222
|
}
|
|
218
223
|
|
|
224
|
+
function DeadlineSectionSkeleton({
|
|
225
|
+
section,
|
|
226
|
+
}: {
|
|
227
|
+
section: KanbanDeadlineSection;
|
|
228
|
+
}) {
|
|
229
|
+
return (
|
|
230
|
+
<div
|
|
231
|
+
aria-hidden="true"
|
|
232
|
+
className="space-y-2"
|
|
233
|
+
data-testid={`kanban-deadline-section-${section}-loading`}
|
|
234
|
+
>
|
|
235
|
+
{Array.from({ length: 3 }, (_, index) => (
|
|
236
|
+
<div
|
|
237
|
+
className="rounded-lg border border-border/60 bg-background/60 p-3"
|
|
238
|
+
key={index}
|
|
239
|
+
>
|
|
240
|
+
<div className="h-3 w-3/4 animate-pulse rounded bg-muted" />
|
|
241
|
+
<div className="mt-3 h-2 w-1/2 animate-pulse rounded bg-muted" />
|
|
242
|
+
<div className="mt-2 flex gap-2">
|
|
243
|
+
<div className="h-2 w-14 animate-pulse rounded bg-muted" />
|
|
244
|
+
<div className="h-2 w-10 animate-pulse rounded bg-muted" />
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
219
252
|
function DeadlineSection({
|
|
220
253
|
availableLists,
|
|
221
254
|
boardId,
|
|
@@ -223,9 +256,12 @@ function DeadlineSection({
|
|
|
223
256
|
collapsed,
|
|
224
257
|
config,
|
|
225
258
|
deadlineNow,
|
|
259
|
+
loading,
|
|
226
260
|
labels,
|
|
227
261
|
isMultiSelectMode,
|
|
228
262
|
isPersonalWorkspace,
|
|
263
|
+
canUseBoardAssignees,
|
|
264
|
+
assigneeMemberSource,
|
|
229
265
|
onClearSelection,
|
|
230
266
|
onCollapsedChange,
|
|
231
267
|
onPinnedChange,
|
|
@@ -234,6 +270,7 @@ function DeadlineSection({
|
|
|
234
270
|
optimisticUpdateInProgress,
|
|
235
271
|
pinned,
|
|
236
272
|
selectedTasks,
|
|
273
|
+
stickyOffset,
|
|
237
274
|
taskLists,
|
|
238
275
|
tasks,
|
|
239
276
|
workspaceId,
|
|
@@ -244,9 +281,12 @@ function DeadlineSection({
|
|
|
244
281
|
collapsed: boolean;
|
|
245
282
|
config: DeadlineSectionConfig;
|
|
246
283
|
deadlineNow?: number;
|
|
284
|
+
loading?: boolean;
|
|
247
285
|
labels: KanbanDeadlineLabels;
|
|
248
286
|
isMultiSelectMode: boolean;
|
|
249
287
|
isPersonalWorkspace: boolean;
|
|
288
|
+
canUseBoardAssignees?: boolean;
|
|
289
|
+
assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
|
|
250
290
|
onClearSelection: () => void;
|
|
251
291
|
onCollapsedChange?: (
|
|
252
292
|
section: KanbanDeadlineSection,
|
|
@@ -258,6 +298,7 @@ function DeadlineSection({
|
|
|
258
298
|
optimisticUpdateInProgress: Set<string>;
|
|
259
299
|
pinned?: boolean;
|
|
260
300
|
selectedTasks: Set<string>;
|
|
301
|
+
stickyOffset?: string;
|
|
261
302
|
taskLists: TaskList[];
|
|
262
303
|
tasks: Task[];
|
|
263
304
|
workspaceId: string;
|
|
@@ -297,17 +338,23 @@ function DeadlineSection({
|
|
|
297
338
|
return sortDeadlineTasks(filteredTasks, sortBy);
|
|
298
339
|
}, [includeDocuments, includeExternal, sortBy, taskListById, tasks]);
|
|
299
340
|
const filterCount = (includeDocuments ? 0 : 1) + (includeExternal ? 0 : 1);
|
|
300
|
-
|
|
301
|
-
|
|
341
|
+
const stickyStyle: React.CSSProperties | undefined = stickyOffset
|
|
342
|
+
? {
|
|
343
|
+
left: `calc(var(--kanban-snap-left-padding) + ${stickyOffset})`,
|
|
344
|
+
}
|
|
345
|
+
: undefined;
|
|
302
346
|
|
|
303
347
|
if (collapsed) {
|
|
304
348
|
return (
|
|
305
349
|
<Card
|
|
306
350
|
className={cn(
|
|
307
351
|
'group flex h-full w-14 shrink-0 snap-start flex-col items-center overflow-hidden rounded-xl border border-dashed transition-all duration-200 hover:shadow-md',
|
|
352
|
+
stickyOffset && 'sticky z-30',
|
|
308
353
|
config.collapsedClassName
|
|
309
354
|
)}
|
|
355
|
+
data-kanban-pinned-special={stickyOffset ? 'true' : undefined}
|
|
310
356
|
data-testid={`kanban-deadline-section-${config.section}-collapsed`}
|
|
357
|
+
style={stickyStyle}
|
|
311
358
|
>
|
|
312
359
|
<button
|
|
313
360
|
type="button"
|
|
@@ -341,9 +388,12 @@ function DeadlineSection({
|
|
|
341
388
|
<Card
|
|
342
389
|
className={cn(
|
|
343
390
|
'flex h-full w-[var(--kanban-column-width)] shrink-0 snap-start flex-col overflow-hidden rounded-xl border border-dashed shadow-xs',
|
|
391
|
+
stickyOffset && 'sticky z-30',
|
|
344
392
|
config.panelClassName
|
|
345
393
|
)}
|
|
394
|
+
data-kanban-pinned-special={stickyOffset ? 'true' : undefined}
|
|
346
395
|
data-testid={`kanban-deadline-section-${config.section}`}
|
|
396
|
+
style={stickyStyle}
|
|
347
397
|
>
|
|
348
398
|
<div className="flex items-center justify-between gap-3 border-border/70 border-b p-3">
|
|
349
399
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
@@ -510,58 +560,62 @@ function DeadlineSection({
|
|
|
510
560
|
)}
|
|
511
561
|
</Button>
|
|
512
562
|
) : null}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
</Button>
|
|
528
|
-
)}
|
|
563
|
+
<Button
|
|
564
|
+
type="button"
|
|
565
|
+
variant="ghost"
|
|
566
|
+
size="xs"
|
|
567
|
+
className={cn(
|
|
568
|
+
'h-7 w-7 p-0 hover:bg-muted/40',
|
|
569
|
+
config.titleClassName
|
|
570
|
+
)}
|
|
571
|
+
title={collapseLabel}
|
|
572
|
+
aria-label={collapseLabel}
|
|
573
|
+
onClick={() => onCollapsedChange?.(config.section, true)}
|
|
574
|
+
>
|
|
575
|
+
<ChevronLeft className="h-3.5 w-3.5" />
|
|
576
|
+
</Button>
|
|
529
577
|
</div>
|
|
530
578
|
</div>
|
|
531
579
|
|
|
532
580
|
<div className="scrollbar-thin min-h-0 flex-1 space-y-2 overflow-y-auto p-2">
|
|
533
|
-
{visibleTasks.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
581
|
+
{loading && visibleTasks.length === 0 ? (
|
|
582
|
+
<DeadlineSectionSkeleton section={config.section} />
|
|
583
|
+
) : (
|
|
584
|
+
visibleTasks.map((task) => {
|
|
585
|
+
const taskList = getTaskListForDeadlineTask(task, taskLists);
|
|
586
|
+
|
|
587
|
+
return (
|
|
588
|
+
<div
|
|
589
|
+
key={task.id}
|
|
590
|
+
className="shrink-0"
|
|
591
|
+
data-testid={`kanban-deadline-task-card-${task.id}`}
|
|
592
|
+
>
|
|
593
|
+
<TaskCard
|
|
594
|
+
availableLists={availableLists}
|
|
595
|
+
boardId={boardId}
|
|
596
|
+
bulkUpdateCustomDueDate={bulkUpdateCustomDueDate}
|
|
597
|
+
dragDisabled
|
|
598
|
+
isMultiSelectMode={isMultiSelectMode}
|
|
599
|
+
isPersonalWorkspace={isPersonalWorkspace}
|
|
600
|
+
canUseBoardAssignees={canUseBoardAssignees}
|
|
601
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
602
|
+
isSelected={selectedTasks.has(task.id)}
|
|
603
|
+
onClearSelection={onClearSelection}
|
|
604
|
+
onSelect={onTaskSelect}
|
|
605
|
+
onUpdate={onUpdate}
|
|
606
|
+
optimisticUpdateInProgress={optimisticUpdateInProgress}
|
|
607
|
+
selectedTasks={selectedTasks}
|
|
608
|
+
deadlineContext={config.section}
|
|
609
|
+
deadlineNow={deadlineNow}
|
|
610
|
+
sortableId={`deadline-${config.section}-${task.id}`}
|
|
611
|
+
task={task}
|
|
612
|
+
taskList={taskList}
|
|
613
|
+
workspaceId={workspaceId}
|
|
614
|
+
/>
|
|
615
|
+
</div>
|
|
616
|
+
);
|
|
617
|
+
})
|
|
618
|
+
)}
|
|
565
619
|
</div>
|
|
566
620
|
</Card>
|
|
567
621
|
);
|
|
@@ -573,6 +627,8 @@ export function KanbanDeadlinePanels({
|
|
|
573
627
|
bulkUpdateCustomDueDate,
|
|
574
628
|
isMultiSelectMode,
|
|
575
629
|
isPersonalWorkspace,
|
|
630
|
+
canUseBoardAssignees,
|
|
631
|
+
assigneeMemberSource,
|
|
576
632
|
labels,
|
|
577
633
|
onClearSelection,
|
|
578
634
|
onSectionCollapsedChange,
|
|
@@ -581,17 +637,15 @@ export function KanbanDeadlinePanels({
|
|
|
581
637
|
onUpdate,
|
|
582
638
|
optimisticUpdateInProgress,
|
|
583
639
|
sections,
|
|
640
|
+
loading,
|
|
584
641
|
collapsedSections,
|
|
585
642
|
pinnedSections,
|
|
643
|
+
stickyOffsets,
|
|
586
644
|
deadlineNow,
|
|
587
645
|
selectedTasks,
|
|
588
646
|
taskLists,
|
|
589
647
|
workspaceId,
|
|
590
648
|
}: KanbanDeadlinePanelsProps) {
|
|
591
|
-
if (sections.overdue.length === 0 && sections.upcoming.length === 0) {
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
649
|
const configs: DeadlineSectionConfig[] = [
|
|
596
650
|
{
|
|
597
651
|
icon: AlertTriangle,
|
|
@@ -625,8 +679,11 @@ export function KanbanDeadlinePanels({
|
|
|
625
679
|
config={config}
|
|
626
680
|
deadlineNow={deadlineNow}
|
|
627
681
|
labels={labels}
|
|
682
|
+
loading={loading === true && sections[config.section].length === 0}
|
|
628
683
|
isMultiSelectMode={isMultiSelectMode}
|
|
629
684
|
isPersonalWorkspace={isPersonalWorkspace}
|
|
685
|
+
canUseBoardAssignees={canUseBoardAssignees}
|
|
686
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
630
687
|
onClearSelection={onClearSelection}
|
|
631
688
|
onCollapsedChange={onSectionCollapsedChange}
|
|
632
689
|
onPinnedChange={onSectionPinnedChange}
|
|
@@ -635,6 +692,7 @@ export function KanbanDeadlinePanels({
|
|
|
635
692
|
optimisticUpdateInProgress={optimisticUpdateInProgress}
|
|
636
693
|
pinned={pinnedSections?.[config.section] === true}
|
|
637
694
|
selectedTasks={selectedTasks}
|
|
695
|
+
stickyOffset={stickyOffsets?.[config.section]}
|
|
638
696
|
taskLists={taskLists}
|
|
639
697
|
tasks={sections[config.section]}
|
|
640
698
|
workspaceId={workspaceId}
|
|
@@ -71,14 +71,20 @@ function KanbanColumnSkeleton({
|
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export function KanbanSkeleton() {
|
|
74
|
+
export function KanbanSkeleton({ root = false }: { root?: boolean }) {
|
|
75
75
|
return (
|
|
76
76
|
<div
|
|
77
77
|
aria-hidden="true"
|
|
78
78
|
className="h-full overflow-hidden bg-transparent"
|
|
79
79
|
data-testid="kanban-skeleton"
|
|
80
80
|
>
|
|
81
|
-
<div
|
|
81
|
+
<div
|
|
82
|
+
className={cn(
|
|
83
|
+
'flex h-full min-w-0 gap-2 overflow-hidden sm:gap-3',
|
|
84
|
+
root ? 'py-2 pr-0 pl-2' : 'p-2'
|
|
85
|
+
)}
|
|
86
|
+
data-testid="kanban-skeleton-frame"
|
|
87
|
+
>
|
|
82
88
|
<div className="hidden shrink-0 gap-2 sm:flex">
|
|
83
89
|
{RAILS.map((rail) => (
|
|
84
90
|
<div
|
|
@@ -102,6 +102,8 @@ interface Props {
|
|
|
102
102
|
pinned: boolean
|
|
103
103
|
) => void;
|
|
104
104
|
onBulkSelectionActiveChange?: (active: boolean) => void;
|
|
105
|
+
canUseBoardAssignees?: boolean;
|
|
106
|
+
assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
|
|
105
107
|
readOnly?: boolean;
|
|
106
108
|
}
|
|
107
109
|
|
|
@@ -125,6 +127,8 @@ export function KanbanBoard({
|
|
|
125
127
|
specialTaskListPins,
|
|
126
128
|
onSpecialTaskListPinnedChange,
|
|
127
129
|
onBulkSelectionActiveChange,
|
|
130
|
+
canUseBoardAssignees,
|
|
131
|
+
assigneeMemberSource,
|
|
128
132
|
readOnly = false,
|
|
129
133
|
}: Props) {
|
|
130
134
|
const tCommon = useTranslations('common');
|
|
@@ -162,26 +166,28 @@ export function KanbanBoard({
|
|
|
162
166
|
const reorderTaskMutation = useReorderTask(boardId ?? '', workspaceId);
|
|
163
167
|
const { createTask } = useTaskDialog();
|
|
164
168
|
const { weekStartsOn } = useCalendarPreferences();
|
|
169
|
+
const boardAssigneesEnabled = canUseBoardAssignees ?? !workspace.personal;
|
|
165
170
|
|
|
166
171
|
const { data: boardConfig } = useBoardConfig(
|
|
167
172
|
readOnly ? null : boardId,
|
|
168
173
|
readOnly ? null : workspaceId
|
|
169
174
|
);
|
|
170
|
-
const { data: deadlineTasks = [] } =
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
const { data: deadlineTasks = [], isPending: deadlineTasksPending } =
|
|
176
|
+
useQuery({
|
|
177
|
+
enabled: Boolean(boardId) && !readOnly,
|
|
178
|
+
queryFn: () =>
|
|
179
|
+
listKanbanDeadlineTasks({
|
|
180
|
+
boardId: boardId ?? '',
|
|
181
|
+
taskQueryOptions: deadlineTaskQueryOptions,
|
|
182
|
+
workspaceId,
|
|
183
|
+
}),
|
|
184
|
+
queryKey: getKanbanDeadlineTasksQueryKey(
|
|
176
185
|
workspaceId,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
),
|
|
183
|
-
staleTime: 30_000,
|
|
184
|
-
});
|
|
186
|
+
boardId,
|
|
187
|
+
deadlineTaskQueryOptions
|
|
188
|
+
),
|
|
189
|
+
staleTime: 30_000,
|
|
190
|
+
});
|
|
185
191
|
const persistListPositions = useCallback(
|
|
186
192
|
async (updates: Array<{ listId: string; newPosition: number }>) => {
|
|
187
193
|
if (!boardId || updates.length === 0) return;
|
|
@@ -297,6 +303,9 @@ export function KanbanBoard({
|
|
|
297
303
|
// Resources Hooks
|
|
298
304
|
const { workspaceLabels, workspaceProjects, workspaceMembers } =
|
|
299
305
|
useBulkResources({
|
|
306
|
+
boardId,
|
|
307
|
+
canUseBoardAssignees: boardAssigneesEnabled,
|
|
308
|
+
assigneeMemberSource,
|
|
300
309
|
workspace,
|
|
301
310
|
isMultiSelectMode,
|
|
302
311
|
selectedCount: selectedTasks.size,
|
|
@@ -325,6 +334,7 @@ export function KanbanBoard({
|
|
|
325
334
|
columns: orderedRealColumns,
|
|
326
335
|
workspaceLabels,
|
|
327
336
|
workspaceProjects,
|
|
337
|
+
workspaceMembers,
|
|
328
338
|
weekStartsOn,
|
|
329
339
|
setBulkWorking,
|
|
330
340
|
clearSelection,
|
|
@@ -523,6 +533,8 @@ export function KanbanBoard({
|
|
|
523
533
|
boardId={boardId ?? ''}
|
|
524
534
|
workspaceId={workspaceId}
|
|
525
535
|
isPersonalWorkspace={workspace.personal}
|
|
536
|
+
canUseBoardAssignees={boardAssigneesEnabled}
|
|
537
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
526
538
|
cursorsEnabled={cursorsEnabled}
|
|
527
539
|
disableSort={disableSort}
|
|
528
540
|
selectedTasks={readOnly ? new Set<string>() : selectedTasks}
|
|
@@ -545,6 +557,7 @@ export function KanbanBoard({
|
|
|
545
557
|
columnsId={columnsId}
|
|
546
558
|
deadlineLabels={deadlineLabels}
|
|
547
559
|
deadlineSections={deadlineSections}
|
|
560
|
+
deadlineSectionsLoading={deadlineTasksPending}
|
|
548
561
|
deadlineSectionsCollapsed={deadlineSectionsCollapsed}
|
|
549
562
|
deadlineNow={deadlineNow}
|
|
550
563
|
onDeadlineSectionCollapsedChange={onDeadlineSectionCollapsedChange}
|
|
@@ -564,6 +577,8 @@ export function KanbanBoard({
|
|
|
564
577
|
columns={orderedColumns}
|
|
565
578
|
boardId={boardId ?? ''}
|
|
566
579
|
isPersonalWorkspace={workspace.personal}
|
|
580
|
+
canUseBoardAssignees={boardAssigneesEnabled}
|
|
581
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
567
582
|
isMultiSelectMode={isMultiSelectMode}
|
|
568
583
|
selectedTasks={selectedTasks}
|
|
569
584
|
onUpdate={() => {}}
|
|
@@ -72,16 +72,18 @@ const BOARD_ID = '11111111-1111-1111-1111-111111111111';
|
|
|
72
72
|
|
|
73
73
|
type BoardClientElement = ReactElement<{
|
|
74
74
|
currentUserId?: string;
|
|
75
|
+
rootLoading?: boolean;
|
|
75
76
|
workspace: unknown;
|
|
76
77
|
workspaceTier: unknown;
|
|
77
78
|
}>;
|
|
78
79
|
|
|
79
|
-
function renderServerPage() {
|
|
80
|
+
function renderServerPage(options?: { rootLoading?: boolean }) {
|
|
80
81
|
return TaskBoardServerPage({
|
|
81
82
|
params: Promise.resolve({
|
|
82
83
|
boardId: BOARD_ID,
|
|
83
84
|
wsId: 'ws-1',
|
|
84
85
|
}),
|
|
86
|
+
rootLoading: options?.rootLoading,
|
|
85
87
|
}) as Promise<BoardClientElement>;
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -183,4 +185,25 @@ describe('TaskBoardServerPage', () => {
|
|
|
183
185
|
expect(element.props.workspace).toBe(workspace);
|
|
184
186
|
expect(element.props.workspaceTier).toBe('FREE');
|
|
185
187
|
});
|
|
188
|
+
|
|
189
|
+
it('passes full-bleed loading through to the board client when requested', async () => {
|
|
190
|
+
const workspace = {
|
|
191
|
+
id: 'ws-board',
|
|
192
|
+
joined: true,
|
|
193
|
+
personal: false,
|
|
194
|
+
tier: 'FREE',
|
|
195
|
+
};
|
|
196
|
+
mocks.getWorkspaceTaskBoard.mockResolvedValue({
|
|
197
|
+
board: {
|
|
198
|
+
access_type: 'member',
|
|
199
|
+
id: BOARD_ID,
|
|
200
|
+
ws_id: 'ws-board',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
mocks.getWorkspace.mockResolvedValue(workspace);
|
|
204
|
+
|
|
205
|
+
const element = await renderServerPage({ rootLoading: true });
|
|
206
|
+
|
|
207
|
+
expect(element.props.rootLoading).toBe(true);
|
|
208
|
+
});
|
|
186
209
|
});
|
|
@@ -14,6 +14,7 @@ import { getWorkspace } from '@tuturuuu/utils/workspace-helper';
|
|
|
14
14
|
import { headers } from 'next/headers';
|
|
15
15
|
import { notFound, redirect } from 'next/navigation';
|
|
16
16
|
import type { ReactNode } from 'react';
|
|
17
|
+
import type { ViewType } from '../../shared/board-views';
|
|
17
18
|
|
|
18
19
|
interface Props {
|
|
19
20
|
params: Promise<{
|
|
@@ -22,7 +23,9 @@ interface Props {
|
|
|
22
23
|
}>;
|
|
23
24
|
/** Route prefix for tasks URLs. Defaults to '/tasks' (web app). Set to '' for satellite apps. */
|
|
24
25
|
routePrefix?: string;
|
|
26
|
+
defaultView?: ViewType;
|
|
25
27
|
idleBottomIsland?: ReactNode;
|
|
28
|
+
rootLoading?: boolean;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
type AuthorizedWorkspace = Workspace & {
|
|
@@ -71,9 +74,11 @@ async function getAuthorizedBoard(wsId: string, boardId: string) {
|
|
|
71
74
|
* Used by both apps/web and apps/tasks.
|
|
72
75
|
*/
|
|
73
76
|
export default async function TaskBoardServerPage({
|
|
77
|
+
defaultView,
|
|
74
78
|
idleBottomIsland,
|
|
75
79
|
params,
|
|
76
80
|
routePrefix = '/tasks',
|
|
81
|
+
rootLoading = false,
|
|
77
82
|
}: Props) {
|
|
78
83
|
const { wsId: id, boardId } = await params;
|
|
79
84
|
|
|
@@ -95,8 +100,10 @@ export default async function TaskBoardServerPage({
|
|
|
95
100
|
workspace={workspace}
|
|
96
101
|
workspaceTier={workspace.tier ?? null}
|
|
97
102
|
currentUserId={user.id}
|
|
103
|
+
defaultView={defaultView}
|
|
98
104
|
routePrefix={routePrefix}
|
|
99
105
|
idleBottomIsland={idleBottomIsland}
|
|
106
|
+
rootLoading={rootLoading}
|
|
100
107
|
/>
|
|
101
108
|
);
|
|
102
109
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
2
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
3
3
|
import { type MouseEvent, useEffect, useRef } from 'react';
|
|
4
|
-
import { TaskCard } from './task-card';
|
|
4
|
+
import { TaskCard, type TaskCardAssigneeMemberSource } from './task-card';
|
|
5
5
|
|
|
6
6
|
interface MeasuredTaskCardProps {
|
|
7
7
|
task: Task;
|
|
@@ -13,6 +13,8 @@ interface MeasuredTaskCardProps {
|
|
|
13
13
|
isSelected: boolean;
|
|
14
14
|
isMultiSelectMode?: boolean;
|
|
15
15
|
isPersonalWorkspace?: boolean;
|
|
16
|
+
canUseBoardAssignees?: boolean;
|
|
17
|
+
assigneeMemberSource?: TaskCardAssigneeMemberSource;
|
|
16
18
|
onSelect?: (taskId: string, event: MouseEvent) => void;
|
|
17
19
|
onClearSelection?: () => void;
|
|
18
20
|
suppressSortableTransform?: boolean;
|
|
@@ -34,6 +36,8 @@ export function MeasuredTaskCard({
|
|
|
34
36
|
isSelected,
|
|
35
37
|
isMultiSelectMode,
|
|
36
38
|
isPersonalWorkspace,
|
|
39
|
+
canUseBoardAssignees,
|
|
40
|
+
assigneeMemberSource,
|
|
37
41
|
onSelect,
|
|
38
42
|
onClearSelection,
|
|
39
43
|
suppressSortableTransform,
|
|
@@ -99,6 +103,8 @@ export function MeasuredTaskCard({
|
|
|
99
103
|
isSelected={isSelected}
|
|
100
104
|
isMultiSelectMode={isMultiSelectMode}
|
|
101
105
|
isPersonalWorkspace={isPersonalWorkspace}
|
|
106
|
+
canUseBoardAssignees={canUseBoardAssignees}
|
|
107
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
102
108
|
onSelect={onSelect}
|
|
103
109
|
onClearSelection={onClearSelection}
|
|
104
110
|
suppressSortableTransform={suppressSortableTransform}
|
|
@@ -75,6 +75,8 @@ describe('getTaskCardHydratingOpenOptions', () => {
|
|
|
75
75
|
],
|
|
76
76
|
taskWsId: 'source-workspace',
|
|
77
77
|
taskWorkspacePersonal: false,
|
|
78
|
+
canUseBoardAssignees: true,
|
|
79
|
+
assigneeMemberSource: 'workspace',
|
|
78
80
|
initialSharedContext: {
|
|
79
81
|
boardConfig: {
|
|
80
82
|
id: 'source-board',
|
|
@@ -118,10 +120,28 @@ describe('getTaskCardHydratingOpenOptions', () => {
|
|
|
118
120
|
availableLists: [list],
|
|
119
121
|
taskWsId: 'workspace-1',
|
|
120
122
|
taskWorkspacePersonal: true,
|
|
123
|
+
canUseBoardAssignees: false,
|
|
124
|
+
assigneeMemberSource: undefined,
|
|
121
125
|
initialSharedContext: undefined,
|
|
122
126
|
});
|
|
123
127
|
});
|
|
124
128
|
|
|
129
|
+
it('can keep assignees enabled for shared personal board tasks', () => {
|
|
130
|
+
expect(
|
|
131
|
+
getTaskCardHydratingOpenOptions({
|
|
132
|
+
task,
|
|
133
|
+
boardId: 'board-1',
|
|
134
|
+
availableLists: [list],
|
|
135
|
+
canUseBoardAssignees: true,
|
|
136
|
+
effectiveWorkspaceId: 'workspace-1',
|
|
137
|
+
isPersonalWorkspace: true,
|
|
138
|
+
})
|
|
139
|
+
).toMatchObject({
|
|
140
|
+
canUseBoardAssignees: true,
|
|
141
|
+
taskWorkspacePersonal: true,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
125
145
|
it('treats source metadata as an external task snapshot', () => {
|
|
126
146
|
expect(
|
|
127
147
|
isExternalTaskSnapshot({
|
|
@@ -6,6 +6,8 @@ interface TaskCardOpenOptionsInput {
|
|
|
6
6
|
task: Task;
|
|
7
7
|
boardId: string;
|
|
8
8
|
availableLists?: TaskList[];
|
|
9
|
+
assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
|
|
10
|
+
canUseBoardAssignees?: boolean;
|
|
9
11
|
effectiveWorkspaceId?: string;
|
|
10
12
|
isPersonalWorkspace: boolean;
|
|
11
13
|
}
|
|
@@ -96,6 +98,8 @@ export function getTaskCardHydratingOpenOptions({
|
|
|
96
98
|
task,
|
|
97
99
|
boardId,
|
|
98
100
|
availableLists,
|
|
101
|
+
assigneeMemberSource,
|
|
102
|
+
canUseBoardAssignees,
|
|
99
103
|
effectiveWorkspaceId,
|
|
100
104
|
isPersonalWorkspace,
|
|
101
105
|
}: TaskCardOpenOptionsInput) {
|
|
@@ -110,6 +114,9 @@ export function getTaskCardHydratingOpenOptions({
|
|
|
110
114
|
...task,
|
|
111
115
|
list_id: task.source_list_id ?? task.list_id,
|
|
112
116
|
};
|
|
117
|
+
const sourceBoardAssigneesEnabled = sourceWorkspaceId
|
|
118
|
+
? true
|
|
119
|
+
: !isPersonalWorkspace;
|
|
113
120
|
|
|
114
121
|
return {
|
|
115
122
|
initialTask,
|
|
@@ -122,6 +129,9 @@ export function getTaskCardHydratingOpenOptions({
|
|
|
122
129
|
: availableLists,
|
|
123
130
|
taskWsId: sourceWorkspaceId ?? effectiveWorkspaceId,
|
|
124
131
|
taskWorkspacePersonal: sourceWorkspaceId ? false : isPersonalWorkspace,
|
|
132
|
+
canUseBoardAssignees: canUseBoardAssignees ?? sourceBoardAssigneesEnabled,
|
|
133
|
+
assigneeMemberSource:
|
|
134
|
+
assigneeMemberSource ?? (sourceWorkspaceId ? 'workspace' : undefined),
|
|
125
135
|
initialSharedContext,
|
|
126
136
|
};
|
|
127
137
|
}
|