Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: virtualization ish behaviour for issue layouts #3538

Merged
merged 18 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/types/src/issues.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,12 @@ export interface IGroupByColumn {
export interface IIssueMap {
[key: string]: TIssue;
}

export interface IIssueListRow {
id: string;
groupId: string;
type: "HEADER" | "NO_ISSUES" | "QUICK_ADD" | "ISSUE";
name?: string;
icon?: ReactElement | undefined;
payload?: Partial<TIssue>;
}
80 changes: 80 additions & 0 deletions web/components/core/render-if-visible-HOC.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { cn } from "helpers/common.helper";
import React, { useState, useRef, useEffect, ReactNode, MutableRefObject } from "react";

type Props = {
defaultHeight?: string;
verticalOffset?: number;
horizonatlOffset?: number;
root?: MutableRefObject<HTMLElement | null>;
children: ReactNode;
as?: keyof JSX.IntrinsicElements;
classNames?: string;
alwaysRender?: boolean;
placeholderChildren?: ReactNode;
pauseHeightUpdateWhileRendering?: boolean;
changingReference?: any;
};

const RenderIfVisible: React.FC<Props> = (props) => {
const {
defaultHeight = "300px",
root,
verticalOffset = 50,
horizonatlOffset = 0,
as = "div",
children,
classNames = "",
alwaysRender = false, //render the children even if it is not visble in root
placeholderChildren = null, //placeholder children
pauseHeightUpdateWhileRendering = false, //while this is true the height of the blocks are maintained
changingReference, //This is to force render when this reference is changed
} = props;
const [shouldVisible, setShouldVisible] = useState<boolean>(alwaysRender);
const placeholderHeight = useRef<string>(defaultHeight);
const intersectionRef = useRef<HTMLElement | null>(null);

const isVisible = alwaysRender || shouldVisible;

// Set visibility with intersection observer
useEffect(() => {
if (intersectionRef.current) {
const observer = new IntersectionObserver(
(entries) => {
if (typeof window !== undefined && window.requestIdleCallback) {
window.requestIdleCallback(() => setShouldVisible(entries[0].isIntersecting), {
timeout: 300,
});
} else {
setShouldVisible(entries[0].isIntersecting);
}
},
{
root: root?.current,
rootMargin: `${verticalOffset}% ${horizonatlOffset}% ${verticalOffset}% ${horizonatlOffset}%`,
}
);
observer.observe(intersectionRef.current);
return () => {
if (intersectionRef.current) {
observer.unobserve(intersectionRef.current);
}
};
}
}, [root?.current, intersectionRef, children, changingReference]);

//Set height after render
useEffect(() => {
if (intersectionRef.current && isVisible) {
placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`;
}
}, [isVisible, intersectionRef, alwaysRender, pauseHeightUpdateWhileRendering]);

const child = isVisible ? <>{children}</> : placeholderChildren;
const style =
isVisible && !pauseHeightUpdateWhileRendering ? {} : { height: placeholderHeight.current, width: "100%" };
const className = isVisible ? classNames : cn(classNames, "bg-custom-background-80");

return React.createElement(as, { ref: intersectionRef, style, className }, child);
};

export default RenderIfVisible;
11 changes: 9 additions & 2 deletions web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useCallback, useState } from "react";
import { FC, useCallback, useRef, useState } from "react";
import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
Expand Down Expand Up @@ -94,6 +94,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas

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

const scrollableContainerRef = useRef<HTMLDivElement | null>(null);

// states
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
const [dragState, setDragState] = useState<KanbanDragState>({});
Expand Down Expand Up @@ -245,7 +247,10 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
</div>
)}

<div className="horizontal-scroll-enable relative h-full w-full overflow-auto bg-custom-background-90">
<div
className="flex horizontal-scroll-enable relative h-full w-full overflow-auto bg-custom-background-90"
ref={scrollableContainerRef}
>
<div className="relative h-max w-max min-w-full bg-custom-background-90 px-2">
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
{/* drag and delete component */}
Expand Down Expand Up @@ -289,6 +294,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
canEditProperties={canEditProperties}
storeType={storeType}
addIssuesToView={addIssuesToView}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
</DragDropContext>
</div>
Expand Down
38 changes: 26 additions & 12 deletions web/components/issues/issue-layouts/kanban/block.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memo } from "react";
import { MutableRefObject, memo } from "react";
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
// hooks
Expand All @@ -13,6 +13,7 @@ import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
// helper
import { cn } from "helpers/common.helper";
import RenderIfVisible from "components/core/render-if-visible-HOC";

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

interface IssueDetailsBlockProps {
Expand Down Expand Up @@ -107,6 +111,9 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
handleIssues,
quickActions,
canEditProperties,
scrollableContainerRef,
isDragStarted,
issueIds,
} = props;

const issue = issuesMap[issueId];
Expand All @@ -129,24 +136,31 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{issue.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)}
<div
className={cn(
"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",
"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",
{ "hover:cursor-grab": !isDragDisabled },
{ "border-custom-primary-100": snapshot.isDragging },
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
)}
>
<KanbanIssueDetailsBlock
issue={issue}
displayProperties={displayProperties}
handleIssues={handleIssues}
quickActions={quickActions}
isReadOnly={!canEditIssueProperties}
/>
<RenderIfVisible
classNames="space-y-2"
root={scrollableContainerRef}
defaultHeight="100px"
horizonatlOffset={50}
alwaysRender={snapshot.isDragging}
pauseHeightUpdateWhileRendering={isDragStarted}
changingReference={issueIds}
>
<KanbanIssueDetailsBlock
issue={issue}
displayProperties={displayProperties}
handleIssues={handleIssues}
quickActions={quickActions}
isReadOnly={!canEditIssueProperties}
/>
</RenderIfVisible>
</div>
</div>
)}
Expand Down
9 changes: 8 additions & 1 deletion web/components/issues/issue-layouts/kanban/blocks-list.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memo } from "react";
import { MutableRefObject, memo } from "react";
//types
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
Expand All @@ -16,6 +16,8 @@ interface IssueBlocksListProps {
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
}

const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
Expand All @@ -30,6 +32,8 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
handleIssues,
quickActions,
canEditProperties,
scrollableContainerRef,
isDragStarted,
} = props;

return (
Expand All @@ -56,6 +60,9 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
index={index}
isDragDisabled={isDragDisabled}
canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
issueIds={issueIds} //passing to force render for virtualization whenever parent rerenders
/>
);
})}
Expand Down
19 changes: 14 additions & 5 deletions web/components/issues/issue-layouts/kanban/default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { EIssueActions } from "../types";
import { getGroupByColumns } from "../utils";
import { TCreateModalStoreTypes } from "constants/issue";
import { MutableRefObject } from "react";

export interface IGroupByKanBan {
issuesMap: IIssueMap;
Expand All @@ -45,6 +46,8 @@ export interface IGroupByKanBan {
storeType?: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
}

const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
Expand All @@ -67,6 +70,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
storeType,
addIssuesToView,
canEditProperties,
scrollableContainerRef,
isDragStarted,
} = props;

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

return (
<div
className={`relative flex flex-shrink-0 flex-col h-full group ${
groupByVisibilityToggle ? `` : `w-[340px]`
}`}
>
<div className={`relative flex flex-shrink-0 flex-col group ${groupByVisibilityToggle ? `` : `w-[340px]`}`}>
{sub_group_by === null && (
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-custom-background-90 py-1">
<HeaderGroupByCard
Expand Down Expand Up @@ -135,6 +136,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
groupByVisibilityToggle={groupByVisibilityToggle}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
)}
</div>
Expand Down Expand Up @@ -168,6 +171,8 @@ export interface IKanBan {
storeType?: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
}

export const KanBan: React.FC<IKanBan> = observer((props) => {
Expand All @@ -189,6 +194,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
storeType,
addIssuesToView,
canEditProperties,
scrollableContainerRef,
isDragStarted,
} = props;

const issueKanBanView = useKanbanView();
Expand All @@ -213,6 +220,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
storeType={storeType}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
);
});
7 changes: 7 additions & 0 deletions web/components/issues/issue-layouts/kanban/kanban-group.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MutableRefObject } from "react";
import { Droppable } from "@hello-pangea/dnd";
// hooks
import { useProjectState } from "hooks/store";
Expand Down Expand Up @@ -37,6 +38,8 @@ interface IKanbanGroup {
disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
groupByVisibilityToggle: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
}

export const KanbanGroup = (props: IKanbanGroup) => {
Expand All @@ -57,6 +60,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
disableIssueCreation,
quickAddCallback,
viewId,
scrollableContainerRef,
isDragStarted,
} = props;
// hooks
const projectState = useProjectState();
Expand Down Expand Up @@ -127,6 +132,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
handleIssues={handleIssues}
quickActions={quickActions}
canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>

{provided.placeholder}
Expand Down
Loading
Loading