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

[WEB-2316] chore: Kanban group virtualization #5565

Merged
merged 2 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 14 additions & 11 deletions web/core/components/core/render-if-visible-HOC.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Props = {
classNames?: string;
placeholderChildren?: ReactNode;
defaultValue?: boolean;
useIdletime?: boolean;
};

const RenderIfVisible: React.FC<Props> = (props) => {
Expand All @@ -21,9 +22,10 @@ const RenderIfVisible: React.FC<Props> = (props) => {
horizontalOffset = 0,
as = "div",
children,
defaultValue = false,
classNames = "",
placeholderChildren = null, //placeholder children
defaultValue = false,
useIdletime = false,
} = props;
const [shouldVisible, setShouldVisible] = useState<boolean>(defaultValue);
const placeholderHeight = useRef<string>(defaultHeight);
Expand All @@ -37,14 +39,13 @@ const RenderIfVisible: React.FC<Props> = (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,
Expand All @@ -69,8 +70,10 @@ const RenderIfVisible: React.FC<Props> = (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);
};
Expand Down
81 changes: 56 additions & 25 deletions web/core/components/issues/issue-layouts/kanban/default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -53,6 +56,7 @@ export interface IKanBan {
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
showEmptyGroup?: boolean;
subGroupIndex?: number;
}

export const KanBan: React.FC<IKanBan> = observer((props) => {
Expand Down Expand Up @@ -80,6 +84,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
orderBy,
isDropDisabled,
dropErrorMessage,
subGroupIndex = 0,
} = props;

const storeType = useIssueStoreType();
Expand Down Expand Up @@ -133,15 +138,24 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
};

const isGroupByCreatedBy = group_by === "created_by";
const approximateCardHeight = getApproximateCardHeight(displayProperties);
const isSubGroup = !!sub_group_id && sub_group_id !== "null";

return (
<ContentWrapper className={`flex-row relative gap-4 py-4`}>
{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 (
<div
key={subList.id}
Expand All @@ -168,28 +182,45 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
)}

{groupByVisibilityToggle.showIssues && (
<KanbanGroup
groupId={subList.id}
issuesMap={issuesMap}
groupedIssueIds={groupedIssueIds}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
orderBy={orderBy}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
isDropDisabled={!!subList.isDropDisabled || !!isDropDisabled}
dropErrorMessage={subList.dropErrorMessage ?? dropErrorMessage}
updateIssue={updateIssue}
quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef}
loadMoreIssues={loadMoreIssues}
handleOnDrop={handleOnDrop}
/>
<RenderIfVisible
verticalOffset={0}
horizontalOffset={100}
root={scrollableContainerRef}
classNames="relative h-full"
defaultHeight={`${groupHeight}px`}
placeholderChildren={
<KanbanColumnLoader
ignoreHeader
cardHeight={approximateCardHeight}
cardsInColumn={issueLength !== undefined && issueLength < 3 ? issueLength : 3}
/>
}
defaultValue={groupIndex < 5 && subGroupIndex < 2}
useIdletime
>
<KanbanGroup
groupId={subList.id}
issuesMap={issuesMap}
groupedIssueIds={groupedIssueIds}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
orderBy={orderBy}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
isDropDisabled={!!subList.isDropDisabled || !!isDropDisabled}
dropErrorMessage={subList.dropErrorMessage ?? dropErrorMessage}
updateIssue={updateIssue}
quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef}
loadMoreIssues={loadMoreIssues}
handleOnDrop={handleOnDrop}
/>
</RenderIfVisible>
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
<div className="relative h-max min-h-full w-full">
{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 <></>;
Expand Down Expand Up @@ -184,6 +184,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = 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}
Expand Down
51 changes: 51 additions & 0 deletions web/core/components/issues/issue-layouts/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +622 to +672
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve the implementation of getApproximateCardHeight and suggest adding unit tests.

The implementation of getApproximateCardHeight is well-thought-out and aligns with the PR's objectives to dynamically adjust the Kanban card height based on its content. It is recommended to add unit tests to ensure the function behaves as expected under various scenarios, especially with different combinations of display properties.

Would you like me to help by writing some unit tests for this function?

48 changes: 34 additions & 14 deletions web/core/components/ui/loader/layouts/kanban-layout-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
import { forwardRef } from "react";
import { ContentWrapper } from "@plane/ui";

export const KanbanIssueBlockLoader = forwardRef<HTMLSpanElement>((props, ref) => (
<span ref={ref} className="block h-28 animate-pulse bg-custom-background-80 rounded" />
));
export const KanbanIssueBlockLoader = forwardRef<HTMLSpanElement, { cardHeight?: number }>(
({ cardHeight = 100 }, ref) => (
<span
ref={ref}
className={`block animate-pulse bg-custom-background-80 rounded`}
style={{ height: `${cardHeight}px` }}
/>
)
);

export const KanbanColumnLoader = ({
cardsInColumn = 3,
ignoreHeader = false,
cardHeight = 100,
}: {
cardsInColumn?: number;
ignoreHeader?: boolean;
cardHeight?: number;
}) => (
<div className="flex flex-col gap-3">
{!ignoreHeader && (
<div className="flex items-center justify-between h-9 w-80">
<div className="flex item-center gap-3">
<span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
<span className="h-6 w-24 bg-custom-background-80 rounded animate-pulse" />
</div>
</div>
)}
{Array.from({ length: cardsInColumn }, (_, cardIndex) => (
<KanbanIssueBlockLoader key={cardIndex} cardHeight={cardHeight} />
))}
</div>
);

KanbanIssueBlockLoader.displayName = "KanbanIssueBlockLoader";

export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { cardsInEachColumn?: number[] }) => (
<ContentWrapper className="flex-row gap-5 py-1.5 overflow-x-auto">
{cardsInEachColumn.map((cardsInColumn, columnIndex) => (
<div key={columnIndex} className="flex flex-col gap-3">
<div className="flex items-center justify-between h-9 w-80">
<div className="flex item-center gap-3">
<span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
<span className="h-6 w-24 bg-custom-background-80 rounded animate-pulse" />
</div>
</div>
{Array.from({ length: cardsInColumn }, (_, cardIndex) => (
<KanbanIssueBlockLoader key={cardIndex} />
))}
</div>
<KanbanColumnLoader key={columnIndex} cardsInColumn={cardsInColumn} />
))}
</ContentWrapper>
);
Loading