@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
|
@@ -13,6 +13,8 @@ interface DragPreviewProps {
|
|
|
13
13
|
columns: TaskList[];
|
|
14
14
|
boardId: string;
|
|
15
15
|
isPersonalWorkspace: boolean;
|
|
16
|
+
canUseBoardAssignees?: boolean;
|
|
17
|
+
assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
|
|
16
18
|
isMultiSelectMode: boolean;
|
|
17
19
|
selectedTasks: Set<string>;
|
|
18
20
|
onUpdate: () => void;
|
|
@@ -26,6 +28,8 @@ export function DragPreview({
|
|
|
26
28
|
columns,
|
|
27
29
|
boardId,
|
|
28
30
|
isPersonalWorkspace,
|
|
31
|
+
canUseBoardAssignees,
|
|
32
|
+
assigneeMemberSource,
|
|
29
33
|
isMultiSelectMode,
|
|
30
34
|
selectedTasks,
|
|
31
35
|
onUpdate,
|
|
@@ -52,6 +56,8 @@ export function DragPreview({
|
|
|
52
56
|
isOverlay
|
|
53
57
|
onUpdate={onUpdate}
|
|
54
58
|
isPersonalWorkspace={isPersonalWorkspace}
|
|
59
|
+
canUseBoardAssignees={canUseBoardAssignees}
|
|
60
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
55
61
|
/>
|
|
56
62
|
{isMultiCardDrag && (
|
|
57
63
|
<>
|
|
@@ -74,6 +80,8 @@ export function DragPreview({
|
|
|
74
80
|
boardId,
|
|
75
81
|
onUpdate,
|
|
76
82
|
isPersonalWorkspace,
|
|
83
|
+
canUseBoardAssignees,
|
|
84
|
+
assigneeMemberSource,
|
|
77
85
|
isMultiSelectMode,
|
|
78
86
|
selectedTasks,
|
|
79
87
|
]);
|
|
@@ -87,11 +95,22 @@ export function DragPreview({
|
|
|
87
95
|
tasks={tasks.filter((task) => task.list_id === activeColumn.id)}
|
|
88
96
|
isOverlay
|
|
89
97
|
isPersonalWorkspace={isPersonalWorkspace}
|
|
98
|
+
canUseBoardAssignees={canUseBoardAssignees}
|
|
99
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
90
100
|
onUpdate={onUpdate}
|
|
91
101
|
wsId={wsId}
|
|
92
102
|
/>
|
|
93
103
|
) : null,
|
|
94
|
-
[
|
|
104
|
+
[
|
|
105
|
+
activeColumn,
|
|
106
|
+
tasks,
|
|
107
|
+
boardId,
|
|
108
|
+
isPersonalWorkspace,
|
|
109
|
+
canUseBoardAssignees,
|
|
110
|
+
assigneeMemberSource,
|
|
111
|
+
onUpdate,
|
|
112
|
+
wsId,
|
|
113
|
+
]
|
|
95
114
|
);
|
|
96
115
|
|
|
97
116
|
return <>{MemoizedTaskOverlay || MemoizedColumnOverlay}</>;
|
|
@@ -20,9 +20,20 @@ vi.mock('@dnd-kit/sortable', () => ({
|
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
22
|
vi.mock('../../board-column', () => ({
|
|
23
|
-
BoardColumn: ({
|
|
23
|
+
BoardColumn: ({
|
|
24
|
+
column,
|
|
25
|
+
specialPinned,
|
|
26
|
+
specialStickyOffset,
|
|
27
|
+
}: {
|
|
28
|
+
column: TaskList;
|
|
29
|
+
specialPinned?: boolean;
|
|
30
|
+
specialStickyOffset?: string;
|
|
31
|
+
}) => (
|
|
24
32
|
<section
|
|
33
|
+
data-kanban-pinned-special={specialStickyOffset ? 'true' : undefined}
|
|
25
34
|
data-kanban-real-column={column.is_external_staging ? undefined : 'true'}
|
|
35
|
+
data-special-pinned={String(specialPinned === true)}
|
|
36
|
+
data-special-sticky-offset={specialStickyOffset}
|
|
26
37
|
data-testid={`column-${column.id}`}
|
|
27
38
|
/>
|
|
28
39
|
),
|
|
@@ -117,6 +128,20 @@ function task(overrides: Partial<Task>): Task {
|
|
|
117
128
|
};
|
|
118
129
|
}
|
|
119
130
|
|
|
131
|
+
function getMockRect(left: number, right: number) {
|
|
132
|
+
return {
|
|
133
|
+
bottom: 0,
|
|
134
|
+
height: 0,
|
|
135
|
+
left,
|
|
136
|
+
right,
|
|
137
|
+
toJSON: () => ({}),
|
|
138
|
+
top: 0,
|
|
139
|
+
width: right - left,
|
|
140
|
+
x: left,
|
|
141
|
+
y: 0,
|
|
142
|
+
} as DOMRect;
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
describe('KanbanColumns', () => {
|
|
121
146
|
beforeEach(() => {
|
|
122
147
|
cursorOverlayMock.mockClear();
|
|
@@ -525,7 +550,7 @@ describe('KanbanColumns', () => {
|
|
|
525
550
|
).toEqual(['Alpha task', 'Zulu task']);
|
|
526
551
|
});
|
|
527
552
|
|
|
528
|
-
it('anchors the first load on the first real task list when special columns render to the left', () => {
|
|
553
|
+
it('anchors the first load on the first real task list when empty special columns render to the left', () => {
|
|
529
554
|
const frameCallbacks: FrameRequestCallback[] = [];
|
|
530
555
|
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
531
556
|
const originalCancelAnimationFrame = window.cancelAnimationFrame;
|
|
@@ -563,15 +588,9 @@ describe('KanbanColumns', () => {
|
|
|
563
588
|
}}
|
|
564
589
|
deadlineSections={{
|
|
565
590
|
overdue: [],
|
|
566
|
-
upcoming: [
|
|
567
|
-
task({
|
|
568
|
-
end_date: '2026-06-02T00:00:00.000Z',
|
|
569
|
-
id: 'upcoming-task',
|
|
570
|
-
list_id: 'list-1',
|
|
571
|
-
name: 'Upcoming task',
|
|
572
|
-
}),
|
|
573
|
-
],
|
|
591
|
+
upcoming: [],
|
|
574
592
|
}}
|
|
593
|
+
deadlineSectionsLoading
|
|
575
594
|
/>
|
|
576
595
|
);
|
|
577
596
|
const scrollContainer = container.firstElementChild as HTMLElement;
|
|
@@ -614,15 +633,9 @@ describe('KanbanColumns', () => {
|
|
|
614
633
|
}}
|
|
615
634
|
deadlineSections={{
|
|
616
635
|
overdue: [],
|
|
617
|
-
upcoming: [
|
|
618
|
-
task({
|
|
619
|
-
end_date: '2026-06-02T00:00:00.000Z',
|
|
620
|
-
id: 'upcoming-task',
|
|
621
|
-
list_id: 'list-1',
|
|
622
|
-
name: 'Upcoming task',
|
|
623
|
-
}),
|
|
624
|
-
],
|
|
636
|
+
upcoming: [],
|
|
625
637
|
}}
|
|
638
|
+
deadlineSectionsLoading
|
|
626
639
|
/>
|
|
627
640
|
);
|
|
628
641
|
|
|
@@ -633,6 +646,69 @@ describe('KanbanColumns', () => {
|
|
|
633
646
|
}
|
|
634
647
|
});
|
|
635
648
|
|
|
649
|
+
it('keeps the first real task list visible when pinned special lists are sticky', () => {
|
|
650
|
+
const frameCallbacks: FrameRequestCallback[] = [];
|
|
651
|
+
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
652
|
+
const originalCancelAnimationFrame = window.cancelAnimationFrame;
|
|
653
|
+
const collapsedExternalList = {
|
|
654
|
+
...externalList,
|
|
655
|
+
is_external_collapsed: true,
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
window.requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => {
|
|
659
|
+
frameCallbacks.push(callback);
|
|
660
|
+
return frameCallbacks.length;
|
|
661
|
+
});
|
|
662
|
+
window.cancelAnimationFrame = vi.fn();
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
const { container } = render(
|
|
666
|
+
<KanbanColumns
|
|
667
|
+
columns={[collapsedExternalList, ...lists]}
|
|
668
|
+
tasks={[]}
|
|
669
|
+
boardId="board-1"
|
|
670
|
+
workspaceId="ws-1"
|
|
671
|
+
isPersonalWorkspace
|
|
672
|
+
disableSort={false}
|
|
673
|
+
selectedTasks={new Set()}
|
|
674
|
+
isMultiSelectMode={false}
|
|
675
|
+
setIsMultiSelectMode={vi.fn()}
|
|
676
|
+
onTaskSelect={vi.fn()}
|
|
677
|
+
onClearSelection={vi.fn()}
|
|
678
|
+
onUpdate={vi.fn()}
|
|
679
|
+
createTask={vi.fn()}
|
|
680
|
+
taskHeightsRef={{ current: new Map() }}
|
|
681
|
+
optimisticUpdateInProgress={new Set()}
|
|
682
|
+
bulkUpdateCustomDueDate={vi.fn()}
|
|
683
|
+
boardRef={{ current: null }}
|
|
684
|
+
columnsId={[collapsedExternalList, ...lists].map((list) => list.id)}
|
|
685
|
+
specialTaskListPins={{ external_tasks: true }}
|
|
686
|
+
/>
|
|
687
|
+
);
|
|
688
|
+
const scrollContainer = container.firstElementChild as HTMLElement;
|
|
689
|
+
const firstRealColumn = screen.getByTestId('column-list-1');
|
|
690
|
+
const pinnedExternalColumn = screen.getByTestId('column-external-list');
|
|
691
|
+
|
|
692
|
+
Object.defineProperty(firstRealColumn, 'offsetLeft', {
|
|
693
|
+
configurable: true,
|
|
694
|
+
value: 320,
|
|
695
|
+
});
|
|
696
|
+
Object.defineProperty(pinnedExternalColumn, 'getBoundingClientRect', {
|
|
697
|
+
configurable: true,
|
|
698
|
+
value: () => getMockRect(8, 64),
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
act(() => {
|
|
702
|
+
for (const callback of frameCallbacks) callback(0);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
expect(scrollContainer.scrollLeft).toBe(256);
|
|
706
|
+
} finally {
|
|
707
|
+
window.requestAnimationFrame = originalRequestAnimationFrame;
|
|
708
|
+
window.cancelAnimationFrame = originalCancelAnimationFrame;
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
636
712
|
it('renders external deadline cards with their staging list context without exposing the staging list as a move target', () => {
|
|
637
713
|
render(
|
|
638
714
|
<KanbanColumns
|
|
@@ -748,6 +824,110 @@ describe('KanbanColumns', () => {
|
|
|
748
824
|
).toBeInTheDocument();
|
|
749
825
|
});
|
|
750
826
|
|
|
827
|
+
it('keeps pinned deadline sections sticky without forcing them expanded', () => {
|
|
828
|
+
render(
|
|
829
|
+
<KanbanColumns
|
|
830
|
+
columns={lists}
|
|
831
|
+
tasks={[]}
|
|
832
|
+
boardId="board-1"
|
|
833
|
+
workspaceId="ws-1"
|
|
834
|
+
isPersonalWorkspace={false}
|
|
835
|
+
disableSort={false}
|
|
836
|
+
selectedTasks={new Set()}
|
|
837
|
+
isMultiSelectMode={false}
|
|
838
|
+
setIsMultiSelectMode={vi.fn()}
|
|
839
|
+
onTaskSelect={vi.fn()}
|
|
840
|
+
onClearSelection={vi.fn()}
|
|
841
|
+
onUpdate={vi.fn()}
|
|
842
|
+
createTask={vi.fn()}
|
|
843
|
+
taskHeightsRef={{ current: new Map() }}
|
|
844
|
+
optimisticUpdateInProgress={new Set()}
|
|
845
|
+
bulkUpdateCustomDueDate={vi.fn()}
|
|
846
|
+
boardRef={{ current: null }}
|
|
847
|
+
columnsId={lists.map((list) => list.id)}
|
|
848
|
+
deadlineLabels={{
|
|
849
|
+
overdue: 'Overdue',
|
|
850
|
+
upcoming: 'Upcoming',
|
|
851
|
+
}}
|
|
852
|
+
deadlineSections={{
|
|
853
|
+
overdue: [],
|
|
854
|
+
upcoming: [],
|
|
855
|
+
}}
|
|
856
|
+
deadlineSectionsCollapsed={{ overdue: true }}
|
|
857
|
+
specialTaskListPins={{ overdue: true, upcoming: true }}
|
|
858
|
+
/>
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
const collapsedOverdue = screen.getByTestId(
|
|
862
|
+
'kanban-deadline-section-overdue-collapsed'
|
|
863
|
+
);
|
|
864
|
+
const upcoming = screen.getByTestId('kanban-deadline-section-upcoming');
|
|
865
|
+
|
|
866
|
+
expect(collapsedOverdue).toHaveAttribute(
|
|
867
|
+
'data-kanban-pinned-special',
|
|
868
|
+
'true'
|
|
869
|
+
);
|
|
870
|
+
expect(collapsedOverdue).toHaveClass('sticky');
|
|
871
|
+
expect(collapsedOverdue.style.left).toBe(
|
|
872
|
+
'calc(var(--kanban-snap-left-padding) + 0px)'
|
|
873
|
+
);
|
|
874
|
+
expect(upcoming).toHaveAttribute('data-kanban-pinned-special', 'true');
|
|
875
|
+
expect(upcoming).toHaveClass('sticky');
|
|
876
|
+
expect(upcoming.style.left).toBe(
|
|
877
|
+
'calc(var(--kanban-snap-left-padding) + calc(3.5rem + 0.75rem))'
|
|
878
|
+
);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it('assigns sticky offsets to pinned external and closed task lists', () => {
|
|
882
|
+
const collapsedExternalList = {
|
|
883
|
+
...externalList,
|
|
884
|
+
is_external_collapsed: true,
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
render(
|
|
888
|
+
<KanbanColumns
|
|
889
|
+
columns={[collapsedExternalList, ...lists, collapsedClosedList]}
|
|
890
|
+
tasks={[]}
|
|
891
|
+
boardId="board-1"
|
|
892
|
+
workspaceId="ws-1"
|
|
893
|
+
isPersonalWorkspace
|
|
894
|
+
disableSort={false}
|
|
895
|
+
selectedTasks={new Set()}
|
|
896
|
+
isMultiSelectMode={false}
|
|
897
|
+
setIsMultiSelectMode={vi.fn()}
|
|
898
|
+
onTaskSelect={vi.fn()}
|
|
899
|
+
onClearSelection={vi.fn()}
|
|
900
|
+
onUpdate={vi.fn()}
|
|
901
|
+
createTask={vi.fn()}
|
|
902
|
+
taskHeightsRef={{ current: new Map() }}
|
|
903
|
+
optimisticUpdateInProgress={new Set()}
|
|
904
|
+
bulkUpdateCustomDueDate={vi.fn()}
|
|
905
|
+
boardRef={{ current: null }}
|
|
906
|
+
columnsId={[collapsedExternalList, ...lists, collapsedClosedList].map(
|
|
907
|
+
(list) => list.id
|
|
908
|
+
)}
|
|
909
|
+
specialTaskListPins={{ closed_tasks: true, external_tasks: true }}
|
|
910
|
+
/>
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
expect(screen.getByTestId('column-external-list')).toHaveAttribute(
|
|
914
|
+
'data-special-pinned',
|
|
915
|
+
'true'
|
|
916
|
+
);
|
|
917
|
+
expect(screen.getByTestId('column-external-list')).toHaveAttribute(
|
|
918
|
+
'data-special-sticky-offset',
|
|
919
|
+
'0px'
|
|
920
|
+
);
|
|
921
|
+
expect(screen.getByTestId('column-closed-list')).toHaveAttribute(
|
|
922
|
+
'data-special-pinned',
|
|
923
|
+
'true'
|
|
924
|
+
);
|
|
925
|
+
expect(screen.getByTestId('column-closed-list')).toHaveAttribute(
|
|
926
|
+
'data-special-sticky-offset',
|
|
927
|
+
'calc(3.5rem + 0.75rem)'
|
|
928
|
+
);
|
|
929
|
+
});
|
|
930
|
+
|
|
751
931
|
it('passes deadline tick props to upcoming deadline cards', () => {
|
|
752
932
|
render(
|
|
753
933
|
<KanbanColumns
|
|
@@ -800,8 +980,8 @@ describe('KanbanColumns', () => {
|
|
|
800
980
|
);
|
|
801
981
|
});
|
|
802
982
|
|
|
803
|
-
it('
|
|
804
|
-
render(
|
|
983
|
+
it('reserves empty deadline panels before deadline tasks load', () => {
|
|
984
|
+
const { container, rerender } = render(
|
|
805
985
|
<KanbanColumns
|
|
806
986
|
columns={lists}
|
|
807
987
|
tasks={[]}
|
|
@@ -821,15 +1001,77 @@ describe('KanbanColumns', () => {
|
|
|
821
1001
|
bulkUpdateCustomDueDate={vi.fn()}
|
|
822
1002
|
boardRef={{ current: null }}
|
|
823
1003
|
columnsId={lists.map((list) => list.id)}
|
|
1004
|
+
listStatusFilter="all"
|
|
824
1005
|
deadlineLabels={{
|
|
825
1006
|
overdue: 'Overdue',
|
|
826
1007
|
upcoming: 'Upcoming',
|
|
827
1008
|
}}
|
|
828
1009
|
deadlineSections={{ overdue: [], upcoming: [] }}
|
|
1010
|
+
deadlineSectionsLoading
|
|
829
1011
|
/>
|
|
830
1012
|
);
|
|
831
1013
|
|
|
832
|
-
expect(screen.
|
|
1014
|
+
expect(screen.getByTestId('kanban-deadline-panels')).toBeInTheDocument();
|
|
1015
|
+
expect(
|
|
1016
|
+
screen.getByTestId('kanban-deadline-section-overdue')
|
|
1017
|
+
).toBeInTheDocument();
|
|
1018
|
+
expect(
|
|
1019
|
+
screen.getByTestId('kanban-deadline-section-upcoming')
|
|
1020
|
+
).toBeInTheDocument();
|
|
1021
|
+
expect(
|
|
1022
|
+
screen.getByTestId('kanban-deadline-section-overdue-count')
|
|
1023
|
+
).toHaveTextContent('0');
|
|
1024
|
+
expect(
|
|
1025
|
+
screen.getByTestId('kanban-deadline-section-upcoming-count')
|
|
1026
|
+
).toHaveTextContent('0');
|
|
1027
|
+
expect(
|
|
1028
|
+
screen.getByTestId('kanban-deadline-section-overdue-loading')
|
|
1029
|
+
).toBeInTheDocument();
|
|
1030
|
+
expect(
|
|
1031
|
+
screen.getByTestId('kanban-deadline-section-upcoming-loading')
|
|
1032
|
+
).toBeInTheDocument();
|
|
1033
|
+
expect(
|
|
1034
|
+
(container.firstElementChild as HTMLElement).style.getPropertyValue(
|
|
1035
|
+
'--kanban-column-width'
|
|
1036
|
+
)
|
|
1037
|
+
).toContain('/ 4');
|
|
833
1038
|
expect(screen.getByTestId('column-list-1')).toBeInTheDocument();
|
|
1039
|
+
|
|
1040
|
+
rerender(
|
|
1041
|
+
<KanbanColumns
|
|
1042
|
+
columns={lists}
|
|
1043
|
+
tasks={[]}
|
|
1044
|
+
boardId="board-1"
|
|
1045
|
+
workspaceId="ws-1"
|
|
1046
|
+
isPersonalWorkspace={false}
|
|
1047
|
+
disableSort={false}
|
|
1048
|
+
selectedTasks={new Set()}
|
|
1049
|
+
isMultiSelectMode={false}
|
|
1050
|
+
setIsMultiSelectMode={vi.fn()}
|
|
1051
|
+
onTaskSelect={vi.fn()}
|
|
1052
|
+
onClearSelection={vi.fn()}
|
|
1053
|
+
onUpdate={vi.fn()}
|
|
1054
|
+
createTask={vi.fn()}
|
|
1055
|
+
taskHeightsRef={{ current: new Map() }}
|
|
1056
|
+
optimisticUpdateInProgress={new Set()}
|
|
1057
|
+
bulkUpdateCustomDueDate={vi.fn()}
|
|
1058
|
+
boardRef={{ current: null }}
|
|
1059
|
+
columnsId={lists.map((list) => list.id)}
|
|
1060
|
+
listStatusFilter="all"
|
|
1061
|
+
deadlineLabels={{
|
|
1062
|
+
overdue: 'Overdue',
|
|
1063
|
+
upcoming: 'Upcoming',
|
|
1064
|
+
}}
|
|
1065
|
+
deadlineSections={{ overdue: [], upcoming: [] }}
|
|
1066
|
+
/>
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
expect(screen.getByTestId('kanban-deadline-panels')).toBeInTheDocument();
|
|
1070
|
+
expect(
|
|
1071
|
+
screen.queryByTestId('kanban-deadline-section-overdue-loading')
|
|
1072
|
+
).not.toBeInTheDocument();
|
|
1073
|
+
expect(
|
|
1074
|
+
screen.queryByTestId('kanban-deadline-section-upcoming-loading')
|
|
1075
|
+
).not.toBeInTheDocument();
|
|
834
1076
|
});
|
|
835
1077
|
});
|
|
@@ -29,12 +29,17 @@ import {
|
|
|
29
29
|
} from './kanban-deadline-panels';
|
|
30
30
|
import type { KanbanDeadlineSections } from './kanban-deadline-tasks';
|
|
31
31
|
|
|
32
|
+
const KANBAN_COLUMN_GAP = '0.75rem';
|
|
33
|
+
const COLLAPSED_SPECIAL_LIST_WIDTH = '3.5rem';
|
|
34
|
+
|
|
32
35
|
interface KanbanColumnsProps {
|
|
33
36
|
columns: TaskList[];
|
|
34
37
|
tasks: Task[];
|
|
35
38
|
boardId: string;
|
|
36
39
|
workspaceId: string;
|
|
37
40
|
isPersonalWorkspace: boolean;
|
|
41
|
+
canUseBoardAssignees?: boolean;
|
|
42
|
+
assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
|
|
38
43
|
cursorsEnabled?: boolean;
|
|
39
44
|
disableSort: boolean;
|
|
40
45
|
selectedTasks: Set<string>;
|
|
@@ -62,6 +67,7 @@ interface KanbanColumnsProps {
|
|
|
62
67
|
onTaskListCollapsedChange?: (listId: string, collapsed: boolean) => void;
|
|
63
68
|
deadlineLabels?: KanbanDeadlineLabels;
|
|
64
69
|
deadlineSections?: KanbanDeadlineSections;
|
|
70
|
+
deadlineSectionsLoading?: boolean;
|
|
65
71
|
deadlineSectionsCollapsed?: KanbanDeadlineCollapsedState;
|
|
66
72
|
deadlineNow?: number;
|
|
67
73
|
onDeadlineSectionCollapsedChange?: (
|
|
@@ -76,12 +82,52 @@ interface KanbanColumnsProps {
|
|
|
76
82
|
readOnly?: boolean;
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
interface PinnedSpecialListLayout {
|
|
86
|
+
offsets: Record<string, string>;
|
|
87
|
+
totalWidth: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toCalcExpression(parts: string[]) {
|
|
91
|
+
if (parts.length === 0) return '0px';
|
|
92
|
+
if (parts.length === 1) return parts[0] ?? '0px';
|
|
93
|
+
return `calc(${parts.join(' + ')})`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getSpecialListWidth(collapsed: boolean) {
|
|
97
|
+
return collapsed
|
|
98
|
+
? COLLAPSED_SPECIAL_LIST_WIDTH
|
|
99
|
+
: 'var(--kanban-column-width)';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getPinnedSpecialRailWidth(container: HTMLElement) {
|
|
103
|
+
const elements = Array.from(
|
|
104
|
+
container.querySelectorAll<HTMLElement>(
|
|
105
|
+
'[data-kanban-pinned-special="true"]'
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (elements.length === 0) return 0;
|
|
110
|
+
|
|
111
|
+
const rects = elements
|
|
112
|
+
.map((element) => element.getBoundingClientRect())
|
|
113
|
+
.filter((rect) => rect.width > 0);
|
|
114
|
+
|
|
115
|
+
if (rects.length === 0) return 0;
|
|
116
|
+
|
|
117
|
+
const left = Math.min(...rects.map((rect) => rect.left));
|
|
118
|
+
const right = Math.max(...rects.map((rect) => rect.right));
|
|
119
|
+
|
|
120
|
+
return Math.max(0, right - left);
|
|
121
|
+
}
|
|
122
|
+
|
|
79
123
|
export function KanbanColumns({
|
|
80
124
|
columns,
|
|
81
125
|
tasks,
|
|
82
126
|
boardId,
|
|
83
127
|
workspaceId,
|
|
84
128
|
isPersonalWorkspace,
|
|
129
|
+
canUseBoardAssignees,
|
|
130
|
+
assigneeMemberSource,
|
|
85
131
|
cursorsEnabled = true,
|
|
86
132
|
disableSort,
|
|
87
133
|
selectedTasks,
|
|
@@ -104,6 +150,7 @@ export function KanbanColumns({
|
|
|
104
150
|
onTaskListCollapsedChange,
|
|
105
151
|
deadlineLabels,
|
|
106
152
|
deadlineSections,
|
|
153
|
+
deadlineSectionsLoading,
|
|
107
154
|
deadlineSectionsCollapsed,
|
|
108
155
|
deadlineNow,
|
|
109
156
|
onDeadlineSectionCollapsedChange,
|
|
@@ -114,26 +161,78 @@ export function KanbanColumns({
|
|
|
114
161
|
const initialScrollAnchoredBoardRef = useRef<string | null>(null);
|
|
115
162
|
const realColumns = columns.filter((column) => !column.is_external_staging);
|
|
116
163
|
const deadlineSectionOrder: KanbanDeadlineSection[] = ['overdue', 'upcoming'];
|
|
117
|
-
const
|
|
118
|
-
!readOnly && deadlineSections
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
const deadlinePanelsEnabled =
|
|
165
|
+
!readOnly && Boolean(boardId && deadlineSections && deadlineLabels);
|
|
166
|
+
const reservedDeadlineSections = deadlinePanelsEnabled
|
|
167
|
+
? deadlineSectionOrder
|
|
168
|
+
: [];
|
|
169
|
+
const snapEdgePadding =
|
|
170
|
+
columns.length > 0 || reservedDeadlineSections.length > 0
|
|
171
|
+
? '0.5rem'
|
|
172
|
+
: '0px';
|
|
124
173
|
const collapsedColumnCount =
|
|
125
174
|
columns.filter(isKanbanColumnCollapsed).length +
|
|
126
|
-
|
|
175
|
+
reservedDeadlineSections.filter(
|
|
127
176
|
(section) => deadlineSectionsCollapsed?.[section] === true
|
|
128
177
|
).length;
|
|
129
178
|
const dynamicColumnWidth = getKanbanColumnWidth({
|
|
130
|
-
columnCount: columns.length +
|
|
179
|
+
columnCount: columns.length + reservedDeadlineSections.length,
|
|
131
180
|
collapsedColumnCount,
|
|
132
181
|
snapEdgePadding,
|
|
133
182
|
fillAvailableWidth: listStatusFilter === 'all',
|
|
134
183
|
});
|
|
184
|
+
const pinnedSpecialListLayout = (() => {
|
|
185
|
+
const entries: { key: string; width: string }[] = [];
|
|
186
|
+
|
|
187
|
+
if (deadlinePanelsEnabled && specialTaskListPins?.overdue) {
|
|
188
|
+
entries.push({
|
|
189
|
+
key: 'deadline:overdue',
|
|
190
|
+
width: getSpecialListWidth(deadlineSectionsCollapsed?.overdue === true),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (deadlinePanelsEnabled && specialTaskListPins?.upcoming) {
|
|
195
|
+
entries.push({
|
|
196
|
+
key: 'deadline:upcoming',
|
|
197
|
+
width: getSpecialListWidth(
|
|
198
|
+
deadlineSectionsCollapsed?.upcoming === true
|
|
199
|
+
),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const column of columns) {
|
|
204
|
+
const pinned =
|
|
205
|
+
column.is_external_staging === true
|
|
206
|
+
? specialTaskListPins?.external_tasks === true
|
|
207
|
+
: column.status === 'closed'
|
|
208
|
+
? specialTaskListPins?.closed_tasks === true
|
|
209
|
+
: false;
|
|
210
|
+
|
|
211
|
+
if (!pinned) continue;
|
|
212
|
+
|
|
213
|
+
entries.push({
|
|
214
|
+
key: `column:${column.id}`,
|
|
215
|
+
width: getSpecialListWidth(isKanbanColumnCollapsed(column)),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const offsets: Record<string, string> = {};
|
|
220
|
+
const parts: string[] = [];
|
|
221
|
+
|
|
222
|
+
entries.forEach((entry, index) => {
|
|
223
|
+
if (index > 0) parts.push(KANBAN_COLUMN_GAP);
|
|
224
|
+
|
|
225
|
+
offsets[entry.key] = toCalcExpression(parts);
|
|
226
|
+
parts.push(entry.width);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
offsets,
|
|
231
|
+
totalWidth: toCalcExpression(parts),
|
|
232
|
+
} satisfies PinnedSpecialListLayout;
|
|
233
|
+
})();
|
|
135
234
|
const hasLeftSpecialColumns =
|
|
136
|
-
|
|
235
|
+
reservedDeadlineSections.length > 0 ||
|
|
137
236
|
columns.some((column) => column.is_external_staging);
|
|
138
237
|
|
|
139
238
|
useLayoutEffect(() => {
|
|
@@ -151,11 +250,16 @@ export function KanbanColumns({
|
|
|
151
250
|
initialScrollAnchoredBoardRef.current = boardId;
|
|
152
251
|
|
|
153
252
|
const anchor = () => {
|
|
154
|
-
|
|
253
|
+
const pinnedRailWidth = getPinnedSpecialRailWidth(container);
|
|
254
|
+
container.scrollLeft = Math.max(
|
|
255
|
+
0,
|
|
256
|
+
target.offsetLeft - pinnedRailWidth - 8
|
|
257
|
+
);
|
|
155
258
|
};
|
|
156
259
|
|
|
260
|
+
anchor();
|
|
261
|
+
|
|
157
262
|
if (typeof window.requestAnimationFrame !== 'function') {
|
|
158
|
-
anchor();
|
|
159
263
|
return;
|
|
160
264
|
}
|
|
161
265
|
|
|
@@ -172,7 +276,10 @@ export function KanbanColumns({
|
|
|
172
276
|
'--kanban-snap-left-padding': snapEdgePadding,
|
|
173
277
|
'--kanban-snap-right-padding': snapEdgePadding,
|
|
174
278
|
'--kanban-column-width': dynamicColumnWidth,
|
|
175
|
-
scrollPaddingLeft:
|
|
279
|
+
scrollPaddingLeft:
|
|
280
|
+
pinnedSpecialListLayout.totalWidth === '0px'
|
|
281
|
+
? 'var(--kanban-snap-left-padding)'
|
|
282
|
+
: `calc(var(--kanban-snap-left-padding) + ${pinnedSpecialListLayout.totalWidth})`,
|
|
176
283
|
scrollPaddingRight: 'var(--kanban-snap-right-padding)',
|
|
177
284
|
} as React.CSSProperties
|
|
178
285
|
}
|
|
@@ -188,13 +295,15 @@ export function KanbanColumns({
|
|
|
188
295
|
paddingRight: 'var(--kanban-snap-right-padding)',
|
|
189
296
|
}}
|
|
190
297
|
>
|
|
191
|
-
{
|
|
298
|
+
{deadlinePanelsEnabled && deadlineSections && deadlineLabels && (
|
|
192
299
|
<KanbanDeadlinePanels
|
|
193
300
|
availableLists={realColumns}
|
|
194
301
|
boardId={boardId}
|
|
195
302
|
bulkUpdateCustomDueDate={bulkUpdateCustomDueDate}
|
|
196
303
|
isMultiSelectMode={isMultiSelectMode}
|
|
197
304
|
isPersonalWorkspace={isPersonalWorkspace}
|
|
305
|
+
canUseBoardAssignees={canUseBoardAssignees}
|
|
306
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
198
307
|
labels={deadlineLabels}
|
|
199
308
|
onClearSelection={onClearSelection}
|
|
200
309
|
onSectionCollapsedChange={onDeadlineSectionCollapsedChange}
|
|
@@ -202,12 +311,17 @@ export function KanbanColumns({
|
|
|
202
311
|
onUpdate={onUpdate}
|
|
203
312
|
optimisticUpdateInProgress={optimisticUpdateInProgress}
|
|
204
313
|
sections={deadlineSections}
|
|
314
|
+
loading={deadlineSectionsLoading}
|
|
205
315
|
collapsedSections={deadlineSectionsCollapsed}
|
|
206
316
|
deadlineNow={deadlineNow}
|
|
207
317
|
pinnedSections={{
|
|
208
318
|
overdue: specialTaskListPins?.overdue,
|
|
209
319
|
upcoming: specialTaskListPins?.upcoming,
|
|
210
320
|
}}
|
|
321
|
+
stickyOffsets={{
|
|
322
|
+
overdue: pinnedSpecialListLayout.offsets['deadline:overdue'],
|
|
323
|
+
upcoming: pinnedSpecialListLayout.offsets['deadline:upcoming'],
|
|
324
|
+
}}
|
|
211
325
|
onSectionPinnedChange={(section, pinned) =>
|
|
212
326
|
onSpecialTaskListPinnedChange?.(section, pinned)
|
|
213
327
|
}
|
|
@@ -261,6 +375,8 @@ export function KanbanColumns({
|
|
|
261
375
|
tasks={listTasks}
|
|
262
376
|
availableLists={realColumns}
|
|
263
377
|
isPersonalWorkspace={isPersonalWorkspace}
|
|
378
|
+
canUseBoardAssignees={canUseBoardAssignees}
|
|
379
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
264
380
|
onUpdate={onUpdate}
|
|
265
381
|
onAddTask={() =>
|
|
266
382
|
boardId && createTask(boardId, list.id, realColumns, filters)
|
|
@@ -280,6 +396,9 @@ export function KanbanColumns({
|
|
|
280
396
|
wsId={workspaceId}
|
|
281
397
|
onExternalTasksCollapsedChange={onExternalTasksCollapsedChange}
|
|
282
398
|
onTaskListCollapsedChange={onTaskListCollapsedChange}
|
|
399
|
+
specialStickyOffset={
|
|
400
|
+
pinnedSpecialListLayout.offsets[`column:${list.id}`]
|
|
401
|
+
}
|
|
283
402
|
specialPinned={
|
|
284
403
|
list.is_external_staging
|
|
285
404
|
? specialTaskListPins?.external_tasks === true
|