Virtualization like core changes with intersection observer

This commit is contained in:
rahulramesha 2024-02-01 18:49:31 +05:30
parent 7d08a57be6
commit 08487fb7d4
4 changed files with 137 additions and 3 deletions

View File

@ -221,3 +221,12 @@ export interface IGroupByColumn {
export interface IIssueMap { export interface IIssueMap {
[key: string]: TIssue; [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>;
}

View File

@ -0,0 +1,74 @@
import { cn } from "helpers/common.helper";
import React, { useState, useRef, useEffect, ReactNode, MutableRefObject } from "react";
type Props = {
defaultHeight?: number;
verticalOffset?: number;
horizonatlOffset?: number;
root?: MutableRefObject<HTMLElement | null>;
children: ReactNode;
as?: keyof JSX.IntrinsicElements;
classNames?: string;
alwaysRender?: boolean;
};
const RenderIfVisible: React.FC<Props> = (props) => {
const {
defaultHeight = 300,
root,
verticalOffset = 50,
horizonatlOffset = 0,
as = "div",
children,
classNames = "",
alwaysRender = false,
...others
} = props;
const [shouldVisible, setShouldVisible] = useState<boolean>(alwaysRender);
const placeholderHeight = useRef<number>(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: 600,
});
} 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]);
// Set height after render
useEffect(() => {
if (intersectionRef.current && isVisible) {
placeholderHeight.current = intersectionRef.current.offsetHeight;
}
}, [isVisible, intersectionRef, alwaysRender]);
const child = isVisible ? <>{children}</> : null;
const style = isVisible ? {} : { height: placeholderHeight.current, width: "100%" };
const className = isVisible ? classNames : cn(classNames, "animate-pulse");
return React.createElement(as, { ref: intersectionRef, style, className }, child);
};
export default RenderIfVisible;

View File

@ -1,10 +1,10 @@
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui"; import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
import { ISSUE_PRIORITIES } from "constants/issue"; import { EIssueListRow, ISSUE_PRIORITIES } from "constants/issue";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
import { IMemberRootStore } from "store/member"; import { IMemberRootStore } from "store/member";
import { IProjectStore } from "store/project/project.store"; import { IProjectStore } from "store/project/project.store";
import { IStateStore } from "store/state.store"; import { IStateStore } from "store/state.store";
import { GroupByColumnTypes, IGroupByColumn } from "@plane/types"; import { GroupByColumnTypes, IGroupByColumn, IIssueListRow, TGroupedIssues, TUnGroupedIssues } from "@plane/types";
import { STATE_GROUPS } from "constants/state"; import { STATE_GROUPS } from "constants/state";
import { ILabelStore } from "store/label.store"; import { ILabelStore } from "store/label.store";
@ -155,3 +155,47 @@ const getCreatedByColumns = (member: IMemberRootStore) => {
}; };
}); });
}; };
export function getIssueFlatList(
groups: IGroupByColumn[],
issueIds: TGroupedIssues | TUnGroupedIssues,
showEmptyGroup: boolean
): IIssueListRow[] | undefined {
let list: IIssueListRow[] = [];
if (Array.isArray(issueIds)) {
return wrapIssuesWithHeaderAndFooter(groups[0], issueIds, showEmptyGroup);
}
for (const group of groups) {
const groupList = wrapIssuesWithHeaderAndFooter(group, issueIds[group.id], showEmptyGroup);
if (!groupList) continue;
list = list.concat(groupList);
}
return list;
}
function wrapIssuesWithHeaderAndFooter(
group: IGroupByColumn,
issueIds: string[],
showEmptyGroup: boolean
): IIssueListRow[] | undefined {
const header: IIssueListRow = { ...group, groupId: group.id, type: EIssueListRow.HEADER };
const quickAdd: IIssueListRow = { ...group, groupId: group.id, type: EIssueListRow.QUICK_ADD };
if (issueIds && issueIds.length > 0) {
const list: IIssueListRow[] = [header];
for (const issueId of issueIds) {
list.push({ id: issueId, groupId: group.id, type: EIssueListRow.ISSUE });
}
list.push(quickAdd);
return list;
}
if (showEmptyGroup) return [header, { id: group.id, groupId: group.id, type: EIssueListRow.NO_ISSUES }, quickAdd];
}

View File

@ -412,6 +412,13 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
}, },
}; };
export enum EIssueListRow {
HEADER = "HEADER",
ISSUE = "ISSUE",
NO_ISSUES = "NO_ISSUES",
QUICK_ADD = "QUICK_ADD",
}
export const getValueFromObject = (object: Object, key: string): string | number | boolean | null => { export const getValueFromObject = (object: Object, key: string): string | number | boolean | null => {
const keys = key ? key.split(".") : []; const keys = key ? key.split(".") : [];