From e2affc3fa66e93d015c486deae05e2ef27f78d85 Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Fri, 9 Feb 2024 15:53:15 +0530 Subject: [PATCH] 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 --- packages/types/src/issues.d.ts | 9 + web/components/core/render-if-visible-HOC.tsx | 80 ++++++ .../issue-layouts/kanban/base-kanban-root.tsx | 11 +- .../issues/issue-layouts/kanban/block.tsx | 38 ++- .../issue-layouts/kanban/blocks-list.tsx | 9 +- .../issues/issue-layouts/kanban/default.tsx | 19 +- .../issue-layouts/kanban/kanban-group.tsx | 7 + .../issues/issue-layouts/kanban/swimlanes.tsx | 9 + .../issue-layouts/list/base-list-root.tsx | 40 ++- .../issues/issue-layouts/list/block.tsx | 97 ++++--- .../issues/issue-layouts/list/blocks-list.tsx | 32 ++- .../issues/issue-layouts/list/default.tsx | 23 +- .../issue-layouts/spreadsheet/issue-row.tsx | 237 +++++++++++------- .../spreadsheet/spreadsheet-table.tsx | 42 ++++ .../spreadsheet/spreadsheet-view.tsx | 38 +-- web/components/issues/issue-layouts/utils.tsx | 4 +- web/constants/issue.ts | 9 +- 17 files changed, 467 insertions(+), 237 deletions(-) create mode 100644 web/components/core/render-if-visible-HOC.tsx diff --git a/packages/types/src/issues.d.ts b/packages/types/src/issues.d.ts index c54943f90..1f4a35dd4 100644 --- a/packages/types/src/issues.d.ts +++ b/packages/types/src/issues.d.ts @@ -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; +} diff --git a/web/components/core/render-if-visible-HOC.tsx b/web/components/core/render-if-visible-HOC.tsx new file mode 100644 index 000000000..26ae15285 --- /dev/null +++ b/web/components/core/render-if-visible-HOC.tsx @@ -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; + children: ReactNode; + as?: keyof JSX.IntrinsicElements; + classNames?: string; + alwaysRender?: boolean; + placeholderChildren?: ReactNode; + pauseHeightUpdateWhileRendering?: boolean; + changingReference?: any; +}; + +const RenderIfVisible: React.FC = (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(alwaysRender); + const placeholderHeight = useRef(defaultHeight); + const intersectionRef = useRef(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; diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 64b132267..36d5e0315 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -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"; @@ -94,6 +94,8 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; + const scrollableContainerRef = useRef(null); + // states const [isDragStarted, setIsDragStarted] = useState(false); const [dragState, setDragState] = useState({}); @@ -245,7 +247,10 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas )} -
+
{/* drag and delete component */} @@ -289,6 +294,8 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas canEditProperties={canEditProperties} storeType={storeType} addIssuesToView={addIssuesToView} + scrollableContainerRef={scrollableContainerRef} + isDragStarted={isDragStarted} />
diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 203ac4938..24cbe9908 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -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 @@ -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; @@ -25,6 +26,9 @@ interface IssueBlockProps { handleIssues: (issue: TIssue, action: EIssueActions) => void; quickActions: (issue: TIssue) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; + scrollableContainerRef?: MutableRefObject; + isDragStarted?: boolean; + issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization } interface IssueDetailsBlockProps { @@ -107,6 +111,9 @@ export const KanbanIssueBlock: React.FC = memo((props) => { handleIssues, quickActions, canEditProperties, + scrollableContainerRef, + isDragStarted, + issueIds, } = props; const issue = issuesMap[issueId]; @@ -129,24 +136,31 @@ export const KanbanIssueBlock: React.FC = memo((props) => { {...provided.dragHandleProps} ref={provided.innerRef} > - {issue.tempId !== undefined && ( -
- )}
- + + +
)} diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index 15c797833..3746111e5 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -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"; @@ -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; + isDragStarted?: boolean; } const KanbanIssueBlocksListMemo: React.FC = (props) => { @@ -30,6 +32,8 @@ const KanbanIssueBlocksListMemo: React.FC = (props) => { handleIssues, quickActions, canEditProperties, + scrollableContainerRef, + isDragStarted, } = props; return ( @@ -56,6 +60,9 @@ const KanbanIssueBlocksListMemo: React.FC = (props) => { index={index} isDragDisabled={isDragDisabled} canEditProperties={canEditProperties} + scrollableContainerRef={scrollableContainerRef} + isDragStarted={isDragStarted} + issueIds={issueIds} //passing to force render for virtualization whenever parent rerenders /> ); })} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index de6c1ddae..f11321944 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -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; @@ -45,6 +46,8 @@ export interface IGroupByKanBan { storeType?: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; + scrollableContainerRef?: MutableRefObject; + isDragStarted?: boolean; } const GroupByKanBan: React.FC = observer((props) => { @@ -67,6 +70,8 @@ const GroupByKanBan: React.FC = observer((props) => { storeType, addIssuesToView, canEditProperties, + scrollableContainerRef, + isDragStarted, } = props; const member = useMember(); @@ -92,11 +97,7 @@ const GroupByKanBan: React.FC = observer((props) => { const groupByVisibilityToggle = visibilityGroupBy(_list); return ( -
+
{sub_group_by === null && (
= observer((props) => { disableIssueCreation={disableIssueCreation} canEditProperties={canEditProperties} groupByVisibilityToggle={groupByVisibilityToggle} + scrollableContainerRef={scrollableContainerRef} + isDragStarted={isDragStarted} /> )}
@@ -168,6 +171,8 @@ export interface IKanBan { storeType?: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; + scrollableContainerRef?: MutableRefObject; + isDragStarted?: boolean; } export const KanBan: React.FC = observer((props) => { @@ -189,6 +194,8 @@ export const KanBan: React.FC = observer((props) => { storeType, addIssuesToView, canEditProperties, + scrollableContainerRef, + isDragStarted, } = props; const issueKanBanView = useKanbanView(); @@ -213,6 +220,8 @@ export const KanBan: React.FC = observer((props) => { storeType={storeType} addIssuesToView={addIssuesToView} canEditProperties={canEditProperties} + scrollableContainerRef={scrollableContainerRef} + isDragStarted={isDragStarted} /> ); }); diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index 1a25c563e..7cbda05e1 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -1,3 +1,4 @@ +import { MutableRefObject } from "react"; import { Droppable } from "@hello-pangea/dnd"; // hooks import { useProjectState } from "hooks/store"; @@ -37,6 +38,8 @@ interface IKanbanGroup { disableIssueCreation?: boolean; canEditProperties: (projectId: string | undefined) => boolean; groupByVisibilityToggle: boolean; + scrollableContainerRef?: MutableRefObject; + isDragStarted?: boolean; } export const KanbanGroup = (props: IKanbanGroup) => { @@ -57,6 +60,8 @@ export const KanbanGroup = (props: IKanbanGroup) => { disableIssueCreation, quickAddCallback, viewId, + scrollableContainerRef, + isDragStarted, } = props; // hooks const projectState = useProjectState(); @@ -127,6 +132,8 @@ export const KanbanGroup = (props: IKanbanGroup) => { handleIssues={handleIssues} quickActions={quickActions} canEditProperties={canEditProperties} + scrollableContainerRef={scrollableContainerRef} + isDragStarted={isDragStarted} /> {provided.placeholder} diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 1b9f27828..5fdb58ef0 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -1,3 +1,4 @@ +import { MutableRefObject } from "react"; import { observer } from "mobx-react-lite"; // components import { KanBan } from "./default"; @@ -80,6 +81,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { viewId?: string ) => Promise; viewId?: string; + scrollableContainerRef?: MutableRefObject; } const SubGroupSwimlane: React.FC = observer((props) => { const { @@ -99,6 +101,8 @@ const SubGroupSwimlane: React.FC = observer((props) => { addIssuesToView, quickAddCallback, viewId, + scrollableContainerRef, + isDragStarted, } = props; const calculateIssueCount = (column_id: string) => { @@ -150,6 +154,8 @@ const SubGroupSwimlane: React.FC = observer((props) => { addIssuesToView={addIssuesToView} quickAddCallback={quickAddCallback} viewId={viewId} + scrollableContainerRef={scrollableContainerRef} + isDragStarted={isDragStarted} />
)} @@ -183,6 +189,7 @@ export interface IKanBanSwimLanes { ) => Promise; viewId?: string; canEditProperties: (projectId: string | undefined) => boolean; + scrollableContainerRef?: MutableRefObject; } export const KanBanSwimLanes: React.FC = observer((props) => { @@ -204,6 +211,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { addIssuesToView, quickAddCallback, viewId, + scrollableContainerRef, } = props; const member = useMember(); @@ -249,6 +257,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { canEditProperties={canEditProperties} quickAddCallback={quickAddCallback} viewId={viewId} + scrollableContainerRef={scrollableContainerRef} /> )}
diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index 8f661a9e6..b1441cff7 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -122,26 +122,24 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { ); return ( - <> -
- -
- +
+ +
); }); diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 1ade285a9..ceec7b219 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -48,64 +48,59 @@ export const IssueBlock: React.FC = observer((props: IssueBlock const projectDetails = getProjectById(issue.project_id); return ( - <> -
- {displayProperties && displayProperties?.key && ( -
- {projectDetails?.identifier}-{issue.sequence_id} -
- )} + "last:border-b-transparent": peekIssue?.issueId !== issue.id + })} + > + {displayProperties && displayProperties?.key && ( +
+ {projectDetails?.identifier}-{issue.sequence_id} +
+ )} - {issue?.tempId !== undefined && ( -
- )} + {issue?.tempId !== undefined && ( +
+ )} - {issue?.is_draft ? ( + {issue?.is_draft ? ( + + {issue.name} + + ) : ( + handleIssuePeekOverview(issue)} + className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + > {issue.name} - ) : ( - handleIssuePeekOverview(issue)} - className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" - > - - {issue.name} - - - )} + + )} -
- {!issue?.tempId ? ( - <> - - {quickActions(issue)} - - ) : ( -
- -
- )} -
+
+ {!issue?.tempId ? ( + <> + + {quickActions(issue)} + + ) : ( +
+ +
+ )}
- +
); }); diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 95ee6c7a8..d3c8d1406 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -1,9 +1,10 @@ -import { FC } from "react"; +import { FC, MutableRefObject } from "react"; // components import { IssueBlock } from "components/issues"; // types import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; import { EIssueActions } from "../types"; +import RenderIfVisible from "components/core/render-if-visible-HOC"; interface Props { issueIds: TGroupedIssues | TUnGroupedIssues | any; @@ -12,27 +13,34 @@ interface Props { handleIssues: (issue: TIssue, action: EIssueActions) => Promise; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; + containerRef: MutableRefObject; } export const IssueBlocksList: FC = (props) => { - const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties } = props; + const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties, containerRef } = props; return (
{issueIds && issueIds.length > 0 ? ( issueIds.map((issueId: string) => { if (!issueId) return null; - return ( - + + + ); }) ) : ( diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index dd6c8da22..373897fda 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -1,5 +1,7 @@ +import { useRef } from "react"; // components import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues"; +import { HeaderGroupByCard } from "./headers/group-by-card"; // hooks import { useLabel, useMember, useProject, useProjectState } from "hooks/store"; // types @@ -10,12 +12,12 @@ import { IIssueDisplayProperties, TIssueMap, TUnGroupedIssues, + IGroupByColumn, } from "@plane/types"; import { EIssueActions } from "../types"; // constants -import { HeaderGroupByCard } from "./headers/group-by-card"; -import { getGroupByColumns } from "../utils"; import { TCreateModalStoreTypes } from "constants/issue"; +import { getGroupByColumns } from "../utils"; export interface IGroupByList { issueIds: TGroupedIssues | TUnGroupedIssues | any; @@ -64,9 +66,11 @@ const GroupByList: React.FC = (props) => { const label = useLabel(); const projectState = useProjectState(); - const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true); + const containerRef = useRef(null); - if (!list) return null; + const groups = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true); + + if (!groups) return null; const prePopulateQuickAddData = (groupByKey: string | null, value: any) => { const defaultState = projectState.projectStates?.find((state) => state.default); @@ -104,11 +108,11 @@ const GroupByList: React.FC = (props) => { const isGroupByCreatedBy = group_by === "created_by"; return ( -
- {list && - list.length > 0 && - list.map( - (_list: any) => +
+ {groups && + groups.length > 0 && + groups.map( + (_list: IGroupByColumn) => validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && (
@@ -131,6 +135,7 @@ const GroupByList: React.FC = (props) => { quickActions={quickActions} displayProperties={displayProperties} canEditProperties={canEditProperties} + containerRef={containerRef} /> )} diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 2a97045fe..840ea39f9 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { Dispatch, MutableRefObject, SetStateAction, useRef, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // icons @@ -7,6 +7,7 @@ import { ChevronRight, MoreHorizontal } from "lucide-react"; import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; // components import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +import RenderIfVisible from "components/core/render-if-visible-HOC"; import { IssueColumn } from "./issue-column"; // ui import { ControlLink, Tooltip } from "@plane/ui"; @@ -32,6 +33,9 @@ interface Props { portalElement: React.MutableRefObject; nestingLevel: number; issueId: string; + isScrolled: MutableRefObject; + containerRef: MutableRefObject; + issueIds: string[]; } export const SpreadsheetIssueRow = observer((props: Props) => { @@ -44,8 +48,96 @@ export const SpreadsheetIssueRow = observer((props: Props) => { handleIssues, quickActions, canEditProperties, + isScrolled, + containerRef, + issueIds, } = props; + const [isExpanded, setExpanded] = useState(false); + const { subIssues: subIssuesStore } = useIssueDetail(); + + const subIssues = subIssuesStore.subIssuesByIssueId(issueId); + + return ( + <> + {/* first column/ issue name and key column */} + } + changingReference={issueIds} + > + + + + {isExpanded && + subIssues && + subIssues.length > 0 && + subIssues.map((subIssueId: string) => ( + + ))} + + ); +}); + +interface IssueRowDetailsProps { + displayProperties: IIssueDisplayProperties; + isEstimateEnabled: boolean; + quickActions: ( + issue: TIssue, + customActionButton?: React.ReactElement, + portalElement?: HTMLDivElement | null + ) => React.ReactNode; + canEditProperties: (projectId: string | undefined) => boolean; + handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + portalElement: React.MutableRefObject; + nestingLevel: number; + issueId: string; + isScrolled: MutableRefObject; + isExpanded: boolean; + setExpanded: Dispatch>; +} + +const IssueRowDetails = observer((props: IssueRowDetailsProps) => { + const { + displayProperties, + issueId, + isEstimateEnabled, + nestingLevel, + portalElement, + handleIssues, + quickActions, + canEditProperties, + isScrolled, + isExpanded, + setExpanded, + } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -54,8 +146,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => { const { peekIssue, setPeekIssue } = useIssueDetail(); // states const [isMenuActive, setIsMenuActive] = useState(false); - const [isExpanded, setExpanded] = useState(false); - const menuActionRef = useRef(null); const handleIssuePeekOverview = (issue: TIssue) => { @@ -66,7 +156,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => { const { subIssues: subIssuesStore, issue } = useIssueDetail(); const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); const paddingLeft = `${nestingLevel * 54}px`; @@ -91,81 +180,77 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
); - if (!issueDetail) return null; const disableUserActions = !canEditProperties(issueDetail.project_id); return ( <> - - {/* first column/ issue name and key column */} - - -
-
- - {getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id} - + +
+
+ + {getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id} + - {canEditProperties(issueDetail.project_id) && ( - - )} -
- - {issueDetail.sub_issues_count > 0 && ( -
- + {canEditProperties(issueDetail.project_id) && ( + )}
- - handleIssuePeekOverview(issueDetail)} - className="clickable w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" - > -
- -
0 && ( +
+
- -
- - - {/* Rest of the columns */} - {SPREADSHEET_PROPERTY_LIST.map((property) => ( + + +
+ )} +
+
+ handleIssuePeekOverview(issueDetail)} + className="clickable w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + > +
+ +
+ {issueDetail.name} +
+
+
+
+ + {/* Rest of the columns */} + {SPREADSHEET_PROPERTY_LIST.map((property) => ( { isEstimateEnabled={isEstimateEnabled} /> ))} - - - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( - - ))} ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx index e63b01dfb..5d45157cc 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx @@ -1,4 +1,5 @@ import { observer } from "mobx-react-lite"; +import { MutableRefObject, useEffect, useRef } from "react"; //types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types"; import { EIssueActions } from "../types"; @@ -21,6 +22,7 @@ type Props = { handleIssues: (issue: TIssue, action: EIssueActions) => Promise; canEditProperties: (projectId: string | undefined) => boolean; portalElement: React.MutableRefObject; + containerRef: MutableRefObject; }; export const SpreadsheetTable = observer((props: Props) => { @@ -34,8 +36,45 @@ export const SpreadsheetTable = observer((props: Props) => { quickActions, handleIssues, canEditProperties, + containerRef, } = props; + // states + const isScrolled = useRef(false); + + const handleScroll = () => { + if (!containerRef.current) return; + const scrollLeft = containerRef.current.scrollLeft; + + const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns + const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers + + //The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly + if (scrollLeft > 0 !== isScrolled.current) { + const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child"); + + for (let i = 0; i < firtColumns.length; i++) { + const shadow = i === 0 ? headerShadow : columnShadow; + if (scrollLeft > 0) { + (firtColumns[i] as HTMLElement).style.boxShadow = shadow; + } else { + (firtColumns[i] as HTMLElement).style.boxShadow = "none"; + } + } + isScrolled.current = scrollLeft > 0; + } + }; + + useEffect(() => { + const currentContainerRef = containerRef.current; + + if (currentContainerRef) currentContainerRef.addEventListener("scroll", handleScroll); + + return () => { + if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll); + }; + }, []); + const handleKeyBoardNavigation = useTableKeyboardNavigation(); return ( @@ -58,6 +97,9 @@ export const SpreadsheetTable = observer((props: Props) => { isEstimateEnabled={isEstimateEnabled} handleIssues={handleIssues} portalElement={portalElement} + containerRef={containerRef} + isScrolled={isScrolled} + issueIds={issueIds} /> ))} diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index e99b17850..1ac815ced 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import React, { useRef } from "react"; import { observer } from "mobx-react-lite"; // components import { Spinner } from "@plane/ui"; @@ -48,8 +48,6 @@ export const SpreadsheetView: React.FC = observer((props) => { enableQuickCreateIssue, disableIssueCreation, } = props; - // states - const isScrolled = useRef(false); // refs const containerRef = useRef(null); const portalRef = useRef(null); @@ -58,39 +56,6 @@ export const SpreadsheetView: React.FC = observer((props) => { const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null; - const handleScroll = () => { - if (!containerRef.current) return; - const scrollLeft = containerRef.current.scrollLeft; - - const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns - const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers - - //The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly - if (scrollLeft > 0 !== isScrolled.current) { - const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child"); - - for (let i = 0; i < firtColumns.length; i++) { - const shadow = i === 0 ? headerShadow : columnShadow; - if (scrollLeft > 0) { - (firtColumns[i] as HTMLElement).style.boxShadow = shadow; - } else { - (firtColumns[i] as HTMLElement).style.boxShadow = "none"; - } - } - isScrolled.current = scrollLeft > 0; - } - }; - - useEffect(() => { - const currentContainerRef = containerRef.current; - - if (currentContainerRef) currentContainerRef.addEventListener("scroll", handleScroll); - - return () => { - if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll); - }; - }, []); - if (!issueIds || issueIds.length === 0) return (
@@ -112,6 +77,7 @@ export const SpreadsheetView: React.FC = observer((props) => { quickActions={quickActions} handleIssues={handleIssues} canEditProperties={canEditProperties} + containerRef={containerRef} />
diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index 83ec363b9..0c3367dc1 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -1,10 +1,10 @@ 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 { IMemberRootStore } from "store/member"; import { IProjectStore } from "store/project/project.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 { ILabelStore } from "store/label.store"; diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 57dff280e..5b6ce8187 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -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 => { const keys = key ? key.split(".") : []; @@ -442,4 +449,4 @@ export const groupReactionEmojis = (reactions: any) => { } return _groupedEmojis; -}; +}; \ No newline at end of file