Skip to content

Commit e2affc3

Browse files
authored
chore: virtualization ish behaviour for issue layouts (#3538)
* Virtualization like core changes with intersection observer * Virtualization like changes for spreadsheet * Virtualization like changes for list * Virtualization like changes for kanban * add logic to render all the issues at once * revert back the changes for list to follow the old pattern of grouping * fix column shadow in spreadsheet for rendering rows * fix constant draggable height while dragging and rendering blocks in kanban * fix height glitch while rendered rows adjust to default height * remove loading animation for issue layouts * reduce requestIdleCallback timer to 300ms * remove logic for index tarcking to force render as the same effect seems to be achieved by removing requestIdleCallback * Fix Kanban droppable height * fix spreadsheet sub issue loading * force change in reference to re render the render if visible component when the order of list changes * add comments and minor changes
1 parent eb4c3a4 commit e2affc3

17 files changed

+466
-236
lines changed

packages/types/src/issues.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,12 @@ export interface IGroupByColumn {
221221
export interface IIssueMap {
222222
[key: string]: TIssue;
223223
}
224+
225+
export interface IIssueListRow {
226+
id: string;
227+
groupId: string;
228+
type: "HEADER" | "NO_ISSUES" | "QUICK_ADD" | "ISSUE";
229+
name?: string;
230+
icon?: ReactElement | undefined;
231+
payload?: Partial<TIssue>;
232+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { cn } from "helpers/common.helper";
2+
import React, { useState, useRef, useEffect, ReactNode, MutableRefObject } from "react";
3+
4+
type Props = {
5+
defaultHeight?: string;
6+
verticalOffset?: number;
7+
horizonatlOffset?: number;
8+
root?: MutableRefObject<HTMLElement | null>;
9+
children: ReactNode;
10+
as?: keyof JSX.IntrinsicElements;
11+
classNames?: string;
12+
alwaysRender?: boolean;
13+
placeholderChildren?: ReactNode;
14+
pauseHeightUpdateWhileRendering?: boolean;
15+
changingReference?: any;
16+
};
17+
18+
const RenderIfVisible: React.FC<Props> = (props) => {
19+
const {
20+
defaultHeight = "300px",
21+
root,
22+
verticalOffset = 50,
23+
horizonatlOffset = 0,
24+
as = "div",
25+
children,
26+
classNames = "",
27+
alwaysRender = false, //render the children even if it is not visble in root
28+
placeholderChildren = null, //placeholder children
29+
pauseHeightUpdateWhileRendering = false, //while this is true the height of the blocks are maintained
30+
changingReference, //This is to force render when this reference is changed
31+
} = props;
32+
const [shouldVisible, setShouldVisible] = useState<boolean>(alwaysRender);
33+
const placeholderHeight = useRef<string>(defaultHeight);
34+
const intersectionRef = useRef<HTMLElement | null>(null);
35+
36+
const isVisible = alwaysRender || shouldVisible;
37+
38+
// Set visibility with intersection observer
39+
useEffect(() => {
40+
if (intersectionRef.current) {
41+
const observer = new IntersectionObserver(
42+
(entries) => {
43+
if (typeof window !== undefined && window.requestIdleCallback) {
44+
window.requestIdleCallback(() => setShouldVisible(entries[0].isIntersecting), {
45+
timeout: 300,
46+
});
47+
} else {
48+
setShouldVisible(entries[0].isIntersecting);
49+
}
50+
},
51+
{
52+
root: root?.current,
53+
rootMargin: `${verticalOffset}% ${horizonatlOffset}% ${verticalOffset}% ${horizonatlOffset}%`,
54+
}
55+
);
56+
observer.observe(intersectionRef.current);
57+
return () => {
58+
if (intersectionRef.current) {
59+
observer.unobserve(intersectionRef.current);
60+
}
61+
};
62+
}
63+
}, [root?.current, intersectionRef, children, changingReference]);
64+
65+
//Set height after render
66+
useEffect(() => {
67+
if (intersectionRef.current && isVisible) {
68+
placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`;
69+
}
70+
}, [isVisible, intersectionRef, alwaysRender, pauseHeightUpdateWhileRendering]);
71+
72+
const child = isVisible ? <>{children}</> : placeholderChildren;
73+
const style =
74+
isVisible && !pauseHeightUpdateWhileRendering ? {} : { height: placeholderHeight.current, width: "100%" };
75+
const className = isVisible ? classNames : cn(classNames, "bg-custom-background-80");
76+
77+
return React.createElement(as, { ref: intersectionRef, style, className }, child);
78+
};
79+
80+
export default RenderIfVisible;

web/components/issues/issue-layouts/kanban/base-kanban-root.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, useCallback, useState } from "react";
1+
import { FC, useCallback, useRef, useState } from "react";
22
import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd";
33
import { useRouter } from "next/router";
44
import { observer } from "mobx-react-lite";
@@ -94,6 +94,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
9494

9595
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
9696

97+
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
98+
9799
// states
98100
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
99101
const [dragState, setDragState] = useState<KanbanDragState>({});
@@ -245,7 +247,10 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
245247
</div>
246248
)}
247249

248-
<div className="horizontal-scroll-enable relative h-full w-full overflow-auto bg-custom-background-90">
250+
<div
251+
className="flex horizontal-scroll-enable relative h-full w-full overflow-auto bg-custom-background-90"
252+
ref={scrollableContainerRef}
253+
>
249254
<div className="relative h-max w-max min-w-full bg-custom-background-90 px-2">
250255
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
251256
{/* drag and delete component */}
@@ -289,6 +294,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
289294
canEditProperties={canEditProperties}
290295
storeType={storeType}
291296
addIssuesToView={addIssuesToView}
297+
scrollableContainerRef={scrollableContainerRef}
298+
isDragStarted={isDragStarted}
292299
/>
293300
</DragDropContext>
294301
</div>

web/components/issues/issue-layouts/kanban/block.tsx

+26-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo } from "react";
1+
import { MutableRefObject, memo } from "react";
22
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
33
import { observer } from "mobx-react-lite";
44
// hooks
@@ -13,6 +13,7 @@ import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
1313
import { EIssueActions } from "../types";
1414
// helper
1515
import { cn } from "helpers/common.helper";
16+
import RenderIfVisible from "components/core/render-if-visible-HOC";
1617

1718
interface IssueBlockProps {
1819
peekIssueId?: string;
@@ -25,6 +26,9 @@ interface IssueBlockProps {
2526
handleIssues: (issue: TIssue, action: EIssueActions) => void;
2627
quickActions: (issue: TIssue) => React.ReactNode;
2728
canEditProperties: (projectId: string | undefined) => boolean;
29+
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
30+
isDragStarted?: boolean;
31+
issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization
2832
}
2933

3034
interface IssueDetailsBlockProps {
@@ -107,6 +111,9 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
107111
handleIssues,
108112
quickActions,
109113
canEditProperties,
114+
scrollableContainerRef,
115+
isDragStarted,
116+
issueIds,
110117
} = props;
111118

112119
const issue = issuesMap[issueId];
@@ -129,24 +136,31 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
129136
{...provided.dragHandleProps}
130137
ref={provided.innerRef}
131138
>
132-
{issue.tempId !== undefined && (
133-
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
134-
)}
135139
<div
136140
className={cn(
137-
"space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm transition-all hover:border-custom-border-400",
141+
"rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm transition-all hover:border-custom-border-400",
138142
{ "hover:cursor-grab": !isDragDisabled },
139143
{ "border-custom-primary-100": snapshot.isDragging },
140144
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
141145
)}
142146
>
143-
<KanbanIssueDetailsBlock
144-
issue={issue}
145-
displayProperties={displayProperties}
146-
handleIssues={handleIssues}
147-
quickActions={quickActions}
148-
isReadOnly={!canEditIssueProperties}
149-
/>
147+
<RenderIfVisible
148+
classNames="space-y-2"
149+
root={scrollableContainerRef}
150+
defaultHeight="100px"
151+
horizonatlOffset={50}
152+
alwaysRender={snapshot.isDragging}
153+
pauseHeightUpdateWhileRendering={isDragStarted}
154+
changingReference={issueIds}
155+
>
156+
<KanbanIssueDetailsBlock
157+
issue={issue}
158+
displayProperties={displayProperties}
159+
handleIssues={handleIssues}
160+
quickActions={quickActions}
161+
isReadOnly={!canEditIssueProperties}
162+
/>
163+
</RenderIfVisible>
150164
</div>
151165
</div>
152166
)}

web/components/issues/issue-layouts/kanban/blocks-list.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo } from "react";
1+
import { MutableRefObject, memo } from "react";
22
//types
33
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
44
import { EIssueActions } from "../types";
@@ -16,6 +16,8 @@ interface IssueBlocksListProps {
1616
handleIssues: (issue: TIssue, action: EIssueActions) => void;
1717
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
1818
canEditProperties: (projectId: string | undefined) => boolean;
19+
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
20+
isDragStarted?: boolean;
1921
}
2022

2123
const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
@@ -30,6 +32,8 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
3032
handleIssues,
3133
quickActions,
3234
canEditProperties,
35+
scrollableContainerRef,
36+
isDragStarted,
3337
} = props;
3438

3539
return (
@@ -56,6 +60,9 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
5660
index={index}
5761
isDragDisabled={isDragDisabled}
5862
canEditProperties={canEditProperties}
63+
scrollableContainerRef={scrollableContainerRef}
64+
isDragStarted={isDragStarted}
65+
issueIds={issueIds} //passing to force render for virtualization whenever parent rerenders
5966
/>
6067
);
6168
})}

web/components/issues/issue-layouts/kanban/default.tsx

+14-5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { EIssueActions } from "../types";
2121
import { getGroupByColumns } from "../utils";
2222
import { TCreateModalStoreTypes } from "constants/issue";
23+
import { MutableRefObject } from "react";
2324

2425
export interface IGroupByKanBan {
2526
issuesMap: IIssueMap;
@@ -45,6 +46,8 @@ export interface IGroupByKanBan {
4546
storeType?: TCreateModalStoreTypes;
4647
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
4748
canEditProperties: (projectId: string | undefined) => boolean;
49+
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
50+
isDragStarted?: boolean;
4851
}
4952

5053
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@@ -67,6 +70,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
6770
storeType,
6871
addIssuesToView,
6972
canEditProperties,
73+
scrollableContainerRef,
74+
isDragStarted,
7075
} = props;
7176

7277
const member = useMember();
@@ -92,11 +97,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
9297
const groupByVisibilityToggle = visibilityGroupBy(_list);
9398

9499
return (
95-
<div
96-
className={`relative flex flex-shrink-0 flex-col h-full group ${
97-
groupByVisibilityToggle ? `` : `w-[340px]`
98-
}`}
99-
>
100+
<div className={`relative flex flex-shrink-0 flex-col group ${groupByVisibilityToggle ? `` : `w-[340px]`}`}>
100101
{sub_group_by === null && (
101102
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-custom-background-90 py-1">
102103
<HeaderGroupByCard
@@ -135,6 +136,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
135136
disableIssueCreation={disableIssueCreation}
136137
canEditProperties={canEditProperties}
137138
groupByVisibilityToggle={groupByVisibilityToggle}
139+
scrollableContainerRef={scrollableContainerRef}
140+
isDragStarted={isDragStarted}
138141
/>
139142
)}
140143
</div>
@@ -168,6 +171,8 @@ export interface IKanBan {
168171
storeType?: TCreateModalStoreTypes;
169172
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
170173
canEditProperties: (projectId: string | undefined) => boolean;
174+
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
175+
isDragStarted?: boolean;
171176
}
172177

173178
export const KanBan: React.FC<IKanBan> = observer((props) => {
@@ -189,6 +194,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
189194
storeType,
190195
addIssuesToView,
191196
canEditProperties,
197+
scrollableContainerRef,
198+
isDragStarted,
192199
} = props;
193200

194201
const issueKanBanView = useKanbanView();
@@ -213,6 +220,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
213220
storeType={storeType}
214221
addIssuesToView={addIssuesToView}
215222
canEditProperties={canEditProperties}
223+
scrollableContainerRef={scrollableContainerRef}
224+
isDragStarted={isDragStarted}
216225
/>
217226
);
218227
});

web/components/issues/issue-layouts/kanban/kanban-group.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { MutableRefObject } from "react";
12
import { Droppable } from "@hello-pangea/dnd";
23
// hooks
34
import { useProjectState } from "hooks/store";
@@ -37,6 +38,8 @@ interface IKanbanGroup {
3738
disableIssueCreation?: boolean;
3839
canEditProperties: (projectId: string | undefined) => boolean;
3940
groupByVisibilityToggle: boolean;
41+
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
42+
isDragStarted?: boolean;
4043
}
4144

4245
export const KanbanGroup = (props: IKanbanGroup) => {
@@ -57,6 +60,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
5760
disableIssueCreation,
5861
quickAddCallback,
5962
viewId,
63+
scrollableContainerRef,
64+
isDragStarted,
6065
} = props;
6166
// hooks
6267
const projectState = useProjectState();
@@ -127,6 +132,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
127132
handleIssues={handleIssues}
128133
quickActions={quickActions}
129134
canEditProperties={canEditProperties}
135+
scrollableContainerRef={scrollableContainerRef}
136+
isDragStarted={isDragStarted}
130137
/>
131138

132139
{provided.placeholder}

0 commit comments

Comments
 (0)