Skip to content

Commit ed0aba4

Browse files
committed
fix(OUT-2104): finalized virtualization of tasks list on list and board.
1 parent eebcaaf commit ed0aba4

3 files changed

Lines changed: 198 additions & 164 deletions

File tree

src/app/globals.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,29 @@ a {
4040
.scroll-thumb:hover {
4141
background-color: rgba(0, 0, 0, 0.5);
4242
}
43+
44+
.List {
45+
overflow: auto;
46+
scrollbar-width: thin;
47+
scrollbar-color: rgba(0, 0, 0, 0.5) transparent;
48+
position: relative;
49+
}
50+
51+
.List::-webkit-scrollbar {
52+
width: 8px;
53+
}
54+
55+
.List::-webkit-scrollbar-track {
56+
background: transparent;
57+
}
58+
59+
.List::-webkit-scrollbar-thumb {
60+
background-color: rgba(0, 0, 0, 0.5);
61+
border-radius: 20px;
62+
margin-right: 1px;
63+
transition: background-color 0.15s ease-in-out;
64+
}
65+
66+
.List::-webkit-scrollbar-thumb:hover {
67+
background-color: rgba(0, 0, 0, 0.5);
68+
}

src/app/ui/TaskBoard.tsx

Lines changed: 2 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,15 @@
33
import { updateTask, updateViewModeSettings } from '@/app/(home)/actions'
44
import { TaskDataFetcher } from '@/app/_fetchers/TaskDataFetcher'
55
import { clientUpdateTask } from '@/app/detail/[task_id]/[user_type]/actions'
6-
import { TaskCardList } from '@/app/detail/ui/TaskCardList'
76
import { TaskBoardAppBridge } from '@/app/ui/TaskBoardAppBridge'
87
import { CustomDragLayer } from '@/components/CustomDragLayer'
98
import { CardDragLayer } from '@/components/cards/CardDragLayer'
10-
import { TaskCard } from '@/components/cards/TaskCard'
119
import { TaskColumn } from '@/components/cards/TaskColumn'
1210
import { TaskRow } from '@/components/cards/TaskRow'
1311
import DashboardEmptyState from '@/components/layouts/EmptyState/DashboardEmptyState'
1412
import { NoFilteredTasksState } from '@/components/layouts/EmptyState/NoFilteredTasksState'
1513
import { FilterBar } from '@/components/layouts/FilterBar'
1614
import { Header } from '@/components/layouts/Header'
17-
import { CustomLink } from '@/hoc/CustomLink'
1815
import { DragDropHandler } from '@/hoc/DragDropHandler'
1916
import { useFilter } from '@/hooks/useFilter'
2017
import { selectTaskBoard, updateWorkflowStateIdByTaskId } from '@/redux/features/taskBoardSlice'
@@ -24,16 +21,14 @@ import { TaskResponse } from '@/types/dto/tasks.dto'
2421
import { CreateViewSettingsDTO } from '@/types/dto/viewSettings.dto'
2522
import { View } from '@/types/interfaces'
2623
import { checkEmptyAssignee } from '@/utils/assignee'
27-
import { getCardHref } from '@/utils/getCardHref'
2824
import { sortTaskByDescendingOrder } from '@/utils/sortTask'
2925
import { prioritizeStartedStates } from '@/utils/workflowStates'
3026
import { UserRole } from '@api/core/types/user'
3127
import { Box, Stack } from '@mui/material'
32-
import { useCallback, useEffect, useRef, useState } from 'react'
28+
import { useCallback, useEffect, useState } from 'react'
3329
import { useSelector } from 'react-redux'
3430
import { z } from 'zod'
35-
import { useVirtualizer } from '@tanstack/react-virtual'
36-
import { WorkflowStateResponse } from '@/types/dto/workflowStates.dto'
31+
import { TasksColumnVirtualizer, TasksRowVirtualizer } from '@/app/ui/VirtualizedTasksLists'
3732

3833
interface TaskBoardProps {
3934
mode: UserRole
@@ -228,160 +223,3 @@ export const TaskBoard = ({ mode, workspace }: TaskBoardProps) => {
228223
</>
229224
)
230225
}
231-
232-
interface TasksVirtualizerProps {
233-
rows: TaskResponse[]
234-
mode: UserRole
235-
token: string | null
236-
}
237-
238-
// virtualization component used in board view
239-
function TasksRowVirtualizer({ rows, mode, token }: TasksVirtualizerProps) {
240-
const parentRef = useRef<HTMLDivElement>(null)
241-
242-
const rowVirtualizer = useVirtualizer({
243-
count: rows.length,
244-
getScrollElement: () => parentRef.current,
245-
estimateSize: useCallback(
246-
(index: number) => {
247-
const task = rows[index]
248-
let estimate = 70
249-
if (task.isArchived) estimate += 24
250-
if (task.dueDate) estimate += 24
251-
if (task.title && task.title.length > 50) estimate += 20
252-
return estimate
253-
},
254-
[rows],
255-
),
256-
measureElement: (element) => element.getBoundingClientRect().height,
257-
overscan: 20,
258-
})
259-
260-
return (
261-
<div
262-
ref={parentRef}
263-
className="List"
264-
style={{
265-
height: `100vh`,
266-
width: '100%',
267-
overflow: 'auto',
268-
columnGap: '6px',
269-
}}
270-
>
271-
<div
272-
style={{
273-
height: `${rowVirtualizer.getTotalSize()}px`,
274-
width: '100%',
275-
position: 'relative',
276-
}}
277-
>
278-
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
279-
<div
280-
key={virtualRow.key}
281-
data-index={virtualRow.index}
282-
ref={(node) => rowVirtualizer.measureElement(node)}
283-
style={{
284-
display: 'flex',
285-
position: 'absolute',
286-
transform: `translateY(${virtualRow.start}px)`,
287-
width: '100%',
288-
}}
289-
>
290-
<div style={{ padding: '3px 0' }}>
291-
<CustomLink
292-
key={rows[virtualRow.index].id}
293-
href={{
294-
pathname: getCardHref(rows[virtualRow.index], mode),
295-
query: { token },
296-
}}
297-
style={{ width: 'fit-content' }}
298-
>
299-
<DragDropHandler
300-
key={rows[virtualRow.index].id}
301-
accept={'taskCard'}
302-
index={virtualRow.index}
303-
task={rows[virtualRow.index]}
304-
draggable
305-
>
306-
<Box>
307-
<TaskCard
308-
task={rows[virtualRow.index]}
309-
key={rows[virtualRow.index].id}
310-
href={{
311-
pathname: getCardHref(rows[virtualRow.index], mode),
312-
query: { token },
313-
}}
314-
/>
315-
</Box>
316-
</DragDropHandler>
317-
</CustomLink>
318-
</div>
319-
</div>
320-
))}
321-
</div>
322-
</div>
323-
)
324-
}
325-
326-
function TasksColumnVirtualizer({ rows, mode, token, list }: TasksVirtualizerProps & { list: WorkflowStateResponse }) {
327-
const parentRef = useRef<HTMLDivElement>(null)
328-
const columnVirtualizer = useVirtualizer({
329-
count: rows.length,
330-
getScrollElement: () => parentRef.current,
331-
estimateSize: () => 44,
332-
measureElement: (element) => element.getBoundingClientRect().height,
333-
overscan: 100,
334-
})
335-
return (
336-
<div
337-
ref={parentRef}
338-
className="List"
339-
style={{
340-
height: `100vh`,
341-
width: '100%',
342-
overflow: 'auto',
343-
columnGap: '6px',
344-
}}
345-
>
346-
<div
347-
style={{
348-
height: `${columnVirtualizer.getTotalSize()}px`,
349-
width: '100%',
350-
position: 'relative',
351-
}}
352-
>
353-
{columnVirtualizer.getVirtualItems().map((virtualRow) => (
354-
<div
355-
key={virtualRow.key}
356-
data-index={virtualRow.index}
357-
ref={(node) => columnVirtualizer.measureElement(node)}
358-
style={{
359-
display: 'flex',
360-
position: 'absolute',
361-
transform: `translateY(${virtualRow.start}px)`,
362-
width: '100%',
363-
}}
364-
>
365-
<div style={{ padding: '3px 0', width: '100%' }}>
366-
<DragDropHandler
367-
key={rows[virtualRow.index].id}
368-
accept={'taskCard'}
369-
index={virtualRow.index}
370-
task={rows[virtualRow.index]}
371-
draggable
372-
>
373-
<TaskCardList
374-
task={rows[virtualRow.index]}
375-
variant="task"
376-
key={rows[virtualRow.index].id}
377-
workflowState={list}
378-
mode={mode}
379-
/>
380-
</DragDropHandler>
381-
</div>
382-
</div>
383-
))}
384-
</div>
385-
</div>
386-
)
387-
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
'use client'
2+
3+
import { TaskCardList } from '@/app/detail/ui/TaskCardList'
4+
import { TaskCard } from '@/components/cards/TaskCard'
5+
import { CustomLink } from '@/hoc/CustomLink'
6+
import { DragDropHandler } from '@/hoc/DragDropHandler'
7+
import { TaskResponse } from '@/types/dto/tasks.dto'
8+
import { WorkflowStateResponse } from '@/types/dto/workflowStates.dto'
9+
import { getCardHref } from '@/utils/getCardHref'
10+
import { UserRole } from '@api/core/types/user'
11+
import { Box } from '@mui/material'
12+
import { useVirtualizer } from '@tanstack/react-virtual'
13+
import { useCallback, useRef } from 'react'
14+
15+
interface TasksVirtualizerProps {
16+
rows: TaskResponse[]
17+
mode: UserRole
18+
token: string | null
19+
}
20+
21+
// virtualization component used in board view
22+
export function TasksRowVirtualizer({ rows, mode, token }: TasksVirtualizerProps) {
23+
const parentRef = useRef<HTMLDivElement>(null)
24+
25+
const rowVirtualizer = useVirtualizer({
26+
count: rows.length,
27+
getScrollElement: () => parentRef.current,
28+
estimateSize: useCallback(
29+
(index: number) => {
30+
const task = rows[index]
31+
let estimate = 70
32+
if (task.isArchived) estimate += 24
33+
if (task.dueDate) estimate += 24
34+
if (task.title && task.title.length > 50) estimate += 20
35+
return estimate
36+
},
37+
[rows],
38+
),
39+
measureElement: (element) => element.getBoundingClientRect().height,
40+
overscan: 25,
41+
})
42+
43+
return (
44+
<div
45+
ref={parentRef}
46+
className="List"
47+
style={{
48+
height: `100vh`,
49+
width: '100%',
50+
overflow: 'auto',
51+
columnGap: '6px',
52+
}}
53+
>
54+
<div
55+
style={{
56+
height: `${rowVirtualizer.getTotalSize()}px`,
57+
width: '100%',
58+
position: 'relative',
59+
}}
60+
>
61+
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
62+
<div
63+
key={virtualRow.key}
64+
data-index={virtualRow.index}
65+
ref={(node) => rowVirtualizer.measureElement(node)}
66+
style={{
67+
display: 'flex',
68+
position: 'absolute',
69+
transform: `translateY(${virtualRow.start}px)`,
70+
width: '100%',
71+
}}
72+
>
73+
<div style={{ padding: '3px 0' }}>
74+
<CustomLink
75+
key={rows[virtualRow.index].id}
76+
href={{
77+
pathname: getCardHref(rows[virtualRow.index], mode),
78+
query: { token },
79+
}}
80+
style={{ width: 'fit-content' }}
81+
>
82+
<DragDropHandler
83+
key={rows[virtualRow.index].id}
84+
accept={'taskCard'}
85+
index={virtualRow.index}
86+
task={rows[virtualRow.index]}
87+
draggable
88+
>
89+
<Box>
90+
<TaskCard
91+
task={rows[virtualRow.index]}
92+
key={rows[virtualRow.index].id}
93+
href={{
94+
pathname: getCardHref(rows[virtualRow.index], mode),
95+
query: { token },
96+
}}
97+
/>
98+
</Box>
99+
</DragDropHandler>
100+
</CustomLink>
101+
</div>
102+
</div>
103+
))}
104+
</div>
105+
</div>
106+
)
107+
}
108+
109+
export function TasksColumnVirtualizer({ rows, mode, list }: TasksVirtualizerProps & { list: WorkflowStateResponse }) {
110+
const parentRef = useRef<HTMLDivElement>(null)
111+
const columnVirtualizer = useVirtualizer({
112+
count: rows.length,
113+
getScrollElement: () => parentRef.current,
114+
estimateSize: () => 44,
115+
measureElement: (element) => element.getBoundingClientRect().height,
116+
overscan: 100,
117+
})
118+
return (
119+
<div
120+
ref={parentRef}
121+
className="List"
122+
style={{
123+
height: `100vh`,
124+
width: '100%',
125+
overflow: 'auto',
126+
columnGap: '6px',
127+
}}
128+
>
129+
<div
130+
style={{
131+
height: `${columnVirtualizer.getTotalSize()}px`,
132+
width: '100%',
133+
position: 'relative',
134+
}}
135+
>
136+
{columnVirtualizer.getVirtualItems().map((virtualRow) => (
137+
<div
138+
key={virtualRow.key}
139+
data-index={virtualRow.index}
140+
ref={(node) => columnVirtualizer.measureElement(node)}
141+
style={{
142+
display: 'flex',
143+
position: 'absolute',
144+
transform: `translateY(${virtualRow.start}px)`,
145+
width: '100%',
146+
}}
147+
>
148+
<div style={{ padding: '3px 0', width: '100%' }}>
149+
<DragDropHandler
150+
key={rows[virtualRow.index].id}
151+
accept={'taskCard'}
152+
index={virtualRow.index}
153+
task={rows[virtualRow.index]}
154+
draggable
155+
>
156+
<TaskCardList
157+
task={rows[virtualRow.index]}
158+
variant="task"
159+
key={rows[virtualRow.index].id}
160+
workflowState={list}
161+
mode={mode}
162+
/>
163+
</DragDropHandler>
164+
</div>
165+
</div>
166+
))}
167+
</div>
168+
</div>
169+
)
170+
}

0 commit comments

Comments
 (0)