From 5e83da9ca1a377eb3728d1174c06986c787ca32a Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:03:49 +0530 Subject: [PATCH] [WEB-2316] chore: Kanban group virtualization (#5565) * kanban group virtualization * minor name change --- .../components/core/render-if-visible-HOC.tsx | 25 +++--- .../issues/issue-layouts/kanban/default.tsx | 81 +++++++++++++------ .../issues/issue-layouts/kanban/swimlanes.tsx | 3 +- .../components/issues/issue-layouts/utils.tsx | 51 ++++++++++++ .../loader/layouts/kanban-layout-loader.tsx | 48 +++++++---- 5 files changed, 157 insertions(+), 51 deletions(-) diff --git a/web/core/components/core/render-if-visible-HOC.tsx b/web/core/components/core/render-if-visible-HOC.tsx index 18c071d146..226e6c235d 100644 --- a/web/core/components/core/render-if-visible-HOC.tsx +++ b/web/core/components/core/render-if-visible-HOC.tsx @@ -11,6 +11,7 @@ type Props = { classNames?: string; placeholderChildren?: ReactNode; defaultValue?: boolean; + useIdletime?: boolean; }; const RenderIfVisible: React.FC = (props) => { @@ -21,9 +22,10 @@ const RenderIfVisible: React.FC = (props) => { horizontalOffset = 0, as = "div", children, - defaultValue = false, classNames = "", placeholderChildren = null, //placeholder children + defaultValue = false, + useIdletime = false, } = props; const [shouldVisible, setShouldVisible] = useState(defaultValue); const placeholderHeight = useRef(defaultHeight); @@ -37,14 +39,13 @@ const RenderIfVisible: React.FC = (props) => { const observer = new IntersectionObserver( (entries) => { //DO no remove comments for future - // if (typeof window !== undefined && window.requestIdleCallback) { - // window.requestIdleCallback(() => setShouldVisible(entries[0].isIntersecting), { - // timeout: 300, - // }); - // } else { - // setShouldVisible(entries[0].isIntersecting); - // } - setShouldVisible(entries[entries.length - 1].isIntersecting); + if (typeof window !== undefined && window.requestIdleCallback && useIdletime) { + window.requestIdleCallback(() => setShouldVisible(entries[entries.length - 1].isIntersecting), { + timeout: 300, + }); + } else { + setShouldVisible(entries[entries.length - 1].isIntersecting); + } }, { root: root?.current, @@ -69,8 +70,10 @@ const RenderIfVisible: React.FC = (props) => { }, [isVisible, intersectionRef]); const child = isVisible ? <>{children} : placeholderChildren; - const style = isVisible ? {} : { height: placeholderHeight.current, width: "100%" }; - const className = isVisible ? classNames : cn(classNames, "bg-custom-background-80"); + const style: { width?: string; height?: string } = isVisible + ? {} + : { height: placeholderHeight.current, width: "100%" }; + const className = isVisible || placeholderChildren ? classNames : cn(classNames, "bg-custom-background-80"); return React.createElement(as, { ref: intersectionRef, style, className }, child); }; diff --git a/web/core/components/issues/issue-layouts/kanban/default.tsx b/web/core/components/issues/issue-layouts/kanban/default.tsx index 128f637fd6..9a92894df0 100644 --- a/web/core/components/issues/issue-layouts/kanban/default.tsx +++ b/web/core/components/issues/issue-layouts/kanban/default.tsx @@ -13,14 +13,17 @@ import { TIssueOrderByOptions, } from "@plane/types"; // constants -// hooks import { ContentWrapper } from "@plane/ui"; +// components +import RenderIfVisible from "@/components/core/render-if-visible-HOC"; +import { KanbanColumnLoader } from "@/components/ui"; +// hooks import { useCycle, useKanbanView, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; // types // parent components import { TRenderQuickActions } from "../list/list-view-types"; -import { getGroupByColumns, isWorkspaceLevel, GroupDropLocation } from "../utils"; +import { getGroupByColumns, isWorkspaceLevel, GroupDropLocation, getApproximateCardHeight } from "../utils"; // components import { HeaderGroupByCard } from "./headers/group-by-card"; import { KanbanGroup } from "./kanban-group"; @@ -53,6 +56,7 @@ export interface IKanBan { scrollableContainerRef?: MutableRefObject; handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise; showEmptyGroup?: boolean; + subGroupIndex?: number; } export const KanBan: React.FC = observer((props) => { @@ -80,6 +84,7 @@ export const KanBan: React.FC = observer((props) => { orderBy, isDropDisabled, dropErrorMessage, + subGroupIndex = 0, } = props; const storeType = useIssueStoreType(); @@ -133,15 +138,24 @@ export const KanBan: React.FC = observer((props) => { }; const isGroupByCreatedBy = group_by === "created_by"; + const approximateCardHeight = getApproximateCardHeight(displayProperties); + const isSubGroup = !!sub_group_id && sub_group_id !== "null"; return ( {list && list.length > 0 && - list.map((subList: IGroupByColumn) => { + list.map((subList: IGroupByColumn, groupIndex) => { const groupByVisibilityToggle = visibilityGroupBy(subList); if (groupByVisibilityToggle.showGroup === false) return <>; + + const issueIds = isSubGroup + ? ((groupedIssueIds as TSubGroupedIssues)?.[subList.id]?.[sub_group_id] ?? []) + : ((groupedIssueIds as TGroupedIssues)?.[subList.id] ?? []); + const issueLength = issueIds?.length as number; + const groupHeight = issueLength * approximateCardHeight; + return (
= observer((props) => { )} {groupByVisibilityToggle.showIssues && ( - + + } + defaultValue={groupIndex < 5 && subGroupIndex < 2} + useIdletime + > + + )}
); diff --git a/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx index 373bb8ef8e..28ffa271c1 100644 --- a/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -155,7 +155,7 @@ const SubGroupSwimlane: React.FC = observer((props) => {
{list && list.length > 0 && - list.map((_list: IGroupByColumn) => { + list.map((_list: IGroupByColumn, subGroupIndex) => { const issueCount = getGroupIssueCount(undefined, _list.id, true) ?? 0; const subGroupByVisibilityToggle = visibilitySubGroupBy(_list, issueCount); if (subGroupByVisibilityToggle.showGroup === false) return <>; @@ -184,6 +184,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { sub_group_by={sub_group_by} group_by={group_by} sub_group_id={_list.id} + subGroupIndex={subGroupIndex} updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} diff --git a/web/core/components/issues/issue-layouts/utils.tsx b/web/core/components/issues/issue-layouts/utils.tsx index ec333ef923..43e23f9664 100644 --- a/web/core/components/issues/issue-layouts/utils.tsx +++ b/web/core/components/issues/issue-layouts/utils.tsx @@ -619,3 +619,54 @@ export const isIssueNew = (issue: TIssue) => { const diff = currentDate.getTime() - createdDate.getTime(); return diff < 30000; }; + +/** + * Returns approximate height of Kanban card based on display properties + * @param displayProperties + * @returns + */ +export function getApproximateCardHeight(displayProperties: IIssueDisplayProperties | undefined) { + if (!displayProperties) return 100; + + // default card height + let cardHeight = 46; + + const clonedProperties = clone(displayProperties); + + // key adds the height for key + if (clonedProperties.key) { + cardHeight += 24; + } + + // Ignore smaller dimension properties + const ignoredProperties: (keyof IIssueDisplayProperties)[] = [ + "key", + "sub_issue_count", + "link", + "attachment_count", + "created_on", + "updated_on", + ]; + + ignoredProperties.forEach((key: keyof IIssueDisplayProperties) => { + delete clonedProperties[key]; + }); + + let propertyCount = 0; + + // count the remaining properties + (Object.keys(clonedProperties) as (keyof IIssueDisplayProperties)[]).forEach((key: keyof IIssueDisplayProperties) => { + if (clonedProperties[key]) { + propertyCount++; + } + }); + + // based on property count, approximate the height of each card + if (propertyCount > 3) { + cardHeight += 60; + } else if (propertyCount > 0) { + cardHeight += 32; + } + + return cardHeight; +} diff --git a/web/core/components/ui/loader/layouts/kanban-layout-loader.tsx b/web/core/components/ui/loader/layouts/kanban-layout-loader.tsx index 82a84ff898..57cfdab933 100644 --- a/web/core/components/ui/loader/layouts/kanban-layout-loader.tsx +++ b/web/core/components/ui/loader/layouts/kanban-layout-loader.tsx @@ -1,26 +1,46 @@ import { forwardRef } from "react"; import { ContentWrapper } from "@plane/ui"; -export const KanbanIssueBlockLoader = forwardRef((props, ref) => ( - -)); +export const KanbanIssueBlockLoader = forwardRef( + ({ cardHeight = 100 }, ref) => ( + + ) +); + +export const KanbanColumnLoader = ({ + cardsInColumn = 3, + ignoreHeader = false, + cardHeight = 100, +}: { + cardsInColumn?: number; + ignoreHeader?: boolean; + cardHeight?: number; +}) => ( +
+ {!ignoreHeader && ( +
+
+ + +
+
+ )} + {Array.from({ length: cardsInColumn }, (_, cardIndex) => ( + + ))} +
+); KanbanIssueBlockLoader.displayName = "KanbanIssueBlockLoader"; export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { cardsInEachColumn?: number[] }) => ( {cardsInEachColumn.map((cardsInColumn, columnIndex) => ( -
-
-
- - -
-
- {Array.from({ length: cardsInColumn }, (_, cardIndex) => ( - - ))} -
+ ))}
);