chore: gantt sidebar and main content scroll sync

This commit is contained in:
Aaryan Khandelwal 2024-01-31 17:47:03 +05:30
parent 3ef0570f6a
commit 5ca137fb92
9 changed files with 100 additions and 54 deletions

View File

@ -1,4 +1,4 @@
import { FC } from "react"; import { FC, useEffect, useRef } from "react";
// hooks // hooks
import { useIssueDetail } from "hooks/store"; import { useIssueDetail } from "hooks/store";
import { useChart } from "../hooks"; import { useChart } from "../hooks";
@ -12,7 +12,7 @@ import { IBlockUpdateData, IGanttBlock } from "../types";
export type GanttChartBlocksProps = { export type GanttChartBlocksProps = {
itemsContainerWidth: number; itemsContainerWidth: number;
blocks: IGanttBlock[] | null; blocks: IGanttBlock[] | null;
blockToRender: (data: any) => React.ReactNode; blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
enableBlockLeftResize: boolean; enableBlockLeftResize: boolean;
enableBlockRightResize: boolean; enableBlockRightResize: boolean;
@ -31,9 +31,12 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
enableBlockMove, enableBlockMove,
showAllBlocks, showAllBlocks,
} = props; } = props;
// refs
const { activeBlock, dispatch } = useChart(); const blocksContainerRef = useRef<HTMLDivElement>(null);
// store hooks
const { peekIssue } = useIssueDetail(); const { peekIssue } = useIssueDetail();
// chart hook
const { activeBlock, dispatch, scrollTop, updateScrollTop } = useChart();
// update the active block on hover // update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => { const updateActiveBlock = (block: IGanttBlock | null) => {
@ -75,10 +78,28 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
}); });
}; };
const handleBlocksScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
updateScrollTop(e.currentTarget.scrollTop);
const sidebarScrollContainer = document.getElementById("gantt-sidebar-scroll-container") as HTMLDivElement;
if (!sidebarScrollContainer) return;
sidebarScrollContainer.scrollTop = e.currentTarget.scrollTop;
};
useEffect(() => {
const blocksContainer = blocksContainerRef.current;
if (!blocksContainer) return;
blocksContainer.scrollTop = scrollTop;
}, [scrollTop]);
return ( return (
<div <div
ref={blocksContainerRef}
className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto" className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto"
style={{ width: `${itemsContainerWidth}px` }} style={{ width: `${itemsContainerWidth}px` }}
onScroll={handleBlocksScroll}
> >
{blocks && {blocks &&
blocks.length > 0 && blocks.length > 0 &&
@ -91,26 +112,26 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
return ( return (
<div <div
key={`block-${block.id}`} key={`block-${block.id}`}
className={cn( className={cn("relative h-11", {
"h-11", "rounded bg-custom-background-80": activeBlock?.id === block.id,
{ "rounded bg-custom-background-80": activeBlock?.id === block.id }, "rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
{ peekIssue?.issueId === block.data.id,
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": })}
peekIssue?.issueId === block.data.id,
}
)}
onMouseEnter={() => updateActiveBlock(block)} onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)} onMouseLeave={() => updateActiveBlock(null)}
> >
{!isBlockVisibleOnChart && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />} {isBlockVisibleOnChart ? (
<ChartDraggable <ChartDraggable
block={block} block={block}
blockToRender={blockToRender} blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)} handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize} enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize} enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove} enableBlockMove={enableBlockMove}
/> />
) : (
<ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
)}
</div> </div>
); );
})} })}

View File

@ -1,4 +1,4 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useRef, useState } from "react";
// icons // icons
// components // components
import { GanttChartBlocks } from "components/gantt-chart"; import { GanttChartBlocks } from "components/gantt-chart";
@ -39,7 +39,7 @@ type ChartViewRootProps = {
loaderTitle: string; loaderTitle: string;
blocks: IGanttBlock[] | null; blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode; blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode; sidebarToRender: (props: any) => React.ReactNode;
enableBlockLeftResize: boolean; enableBlockLeftResize: boolean;
enableBlockRightResize: boolean; enableBlockRightResize: boolean;
@ -69,8 +69,11 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0); const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false); const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null); // blocks state management starts const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null); // blocks state management starts
// refs
const sidebarRef = useRef<HTMLDivElement>(null);
// hooks // hooks
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = useChart(); const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft, scrollTop, updateScrollTop } =
useChart();
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) => const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
blocks && blocks.length > 0 blocks && blocks.length > 0
@ -202,17 +205,25 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
const scrollWidth: number = scrollContainer?.scrollWidth; const scrollWidth: number = scrollContainer?.scrollWidth;
const clientVisibleWidth: number = scrollContainer?.clientWidth; const clientVisibleWidth: number = scrollContainer?.clientWidth;
const currentScrollPosition: number = scrollContainer?.scrollLeft; const currentLeftScrollPosition: number = scrollContainer?.scrollLeft;
updateScrollLeft(currentScrollPosition); updateScrollLeft(currentLeftScrollPosition);
const approxRangeLeft: number = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth; const approxRangeLeft: number = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth); const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth);
if (currentScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView); if (currentLeftScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView);
if (currentScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView); if (currentLeftScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView);
}; };
const onSidebarScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => updateScrollTop(e.currentTarget.scrollTop);
// useEffect(() => {
// const sidebarContainer = sidebarRef.current;
// if (!sidebarContainer) return;
// sidebarContainer.scrollTop = scrollTop;
// }, [scrollTop]);
return ( return (
<div <div
className={`${ className={`${
@ -289,7 +300,14 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
<h6>Duration</h6> <h6>Duration</h6>
</div> </div>
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })} <div
id="gantt-sidebar-scroll-container"
className="max-h-full mt-[12px] overflow-y-auto pl-2.5"
onScroll={onSidebarScroll}
ref={sidebarRef}
>
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
</div>
</div> </div>
<div <div
className="horizontal-scroll-enable relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto" className="horizontal-scroll-enable relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto"

View File

@ -24,6 +24,7 @@ const chartReducer = (state: ChartContextData, action: ChartContextActionPayload
const initialView = "month"; const initialView = "month";
export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
// states;
const [state, dispatch] = useState<ChartContextData>({ const [state, dispatch] = useState<ChartContextData>({
currentView: initialView, currentView: initialView,
currentViewData: currentViewDataWithView(initialView), currentViewData: currentViewDataWithView(initialView),
@ -31,23 +32,23 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
allViews: allViewsWithData, allViews: allViewsWithData,
activeBlock: null, activeBlock: null,
}); });
const [scrollLeft, setScrollLeft] = useState(0); const [scrollLeft, setScrollLeft] = useState(0);
const [scrollTop, setScrollTop] = useState(0);
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => { const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
const newState = chartReducer(state, action); const newState = chartReducer(state, action);
dispatch(() => newState); dispatch(() => newState);
return newState; return newState;
}; };
const updateScrollLeft = (scrollLeft: number) => { const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft);
setScrollLeft(scrollLeft);
}; const updateScrollTop = (scrollTop: number) => setScrollTop(scrollTop);
return ( return (
<ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}> <ChartContext.Provider
value={{ ...state, scrollLeft, updateScrollLeft, scrollTop, updateScrollTop, dispatch: handleDispatch }}
>
{children} {children}
</ChartContext.Provider> </ChartContext.Provider>
); );

View File

@ -7,7 +7,7 @@ import { IGanttBlock } from "../types";
type Props = { type Props = {
block: IGanttBlock; block: IGanttBlock;
blockToRender: (data: any) => React.ReactNode; blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right" | "move") => void; handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right" | "move") => void;
enableBlockLeftResize: boolean; enableBlockLeftResize: boolean;
enableBlockRightResize: boolean; enableBlockRightResize: boolean;
@ -223,6 +223,8 @@ export const ChartDraggable: React.FC<Props> = (props) => {
scrollLeft > block.position.marginLeft + block.position.width; scrollLeft > block.position.marginLeft + block.position.width;
const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth; const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth;
const textDisplacement = scrollLeft - (block.position?.marginLeft ?? 0);
return ( return (
<> <>
{/* move to left side hidden block button */} {/* move to left side hidden block button */}
@ -272,7 +274,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
className={`relative z-[2] flex h-8 w-full items-center rounded ${isMoving ? "pointer-events-none" : ""}`} className={`relative z-[2] flex h-8 w-full items-center rounded ${isMoving ? "pointer-events-none" : ""}`}
onMouseDown={handleBlockMove} onMouseDown={handleBlockMove}
> >
{blockToRender(block.data)} {blockToRender(block.data, textDisplacement)}
</div> </div>
{/* right resize drag handle */} {/* right resize drag handle */}
{enableBlockRightResize && ( {enableBlockRightResize && (

View File

@ -12,7 +12,7 @@ type GanttChartRootProps = {
loaderTitle: string; loaderTitle: string;
blocks: IGanttBlock[] | null; blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode; blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode; sidebarToRender: (props: any) => React.ReactNode;
enableBlockLeftResize?: boolean; enableBlockLeftResize?: boolean;
enableBlockRightResize?: boolean; enableBlockRightResize?: boolean;

View File

@ -1,4 +1,3 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react"; import { MoreVertical } from "lucide-react";
// hooks // hooks
@ -43,9 +42,6 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
showAllBlocks = false, showAllBlocks = false,
} = props; } = props;
const router = useRouter();
const { cycleId } = router.query;
const { activeBlock, dispatch } = useChart(); const { activeBlock, dispatch } = useChart();
const { peekIssue } = useIssueDetail(); const { peekIssue } = useIssueDetail();
@ -105,12 +101,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
<DragDropContext onDragEnd={handleOrderChange}> <DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar"> <Droppable droppableId="gantt-sidebar">
{(droppableProvided) => ( {(droppableProvided) => (
<div <div id={`gantt-sidebar-${viewId}`} ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
id={`gantt-sidebar-${cycleId}`}
className="mt-[12px] max-h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<> <>
{blocks ? ( {blocks ? (
blocks.map((block, index) => { blocks.map((block, index) => {

View File

@ -54,6 +54,8 @@ export type ChartContextActionPayload =
export interface ChartContextReducer extends ChartContextData { export interface ChartContextReducer extends ChartContextData {
scrollLeft: number; scrollLeft: number;
updateScrollLeft: (scrollLeft: number) => void; updateScrollLeft: (scrollLeft: number) => void;
scrollTop: number;
updateScrollTop: (scrollTop: number) => void;
dispatch: (action: ChartContextActionPayload) => void; dispatch: (action: ChartContextActionPayload) => void;
} }

View File

@ -69,7 +69,9 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
loaderTitle="Issues" loaderTitle="Issues"
blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null} blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null}
blockUpdateHandler={updateIssueBlockStructure} blockUpdateHandler={updateIssueBlockStructure}
blockToRender={(data: TIssue) => <IssueGanttBlock data={data} />} blockToRender={(data: TIssue, textDisplacement) => (
<IssueGanttBlock data={data} textDisplacement={textDisplacement} />
)}
sidebarToRender={(props) => ( sidebarToRender={(props) => (
<IssueGanttSidebar <IssueGanttSidebar
{...props} {...props}

View File

@ -1,13 +1,14 @@
// hooks
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
// ui // ui
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui"; import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
// helpers // helpers
import { renderFormattedDate } from "helpers/date-time.helper"; import { renderFormattedDate } from "helpers/date-time.helper";
// types // types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
export const IssueGanttBlock = ({ data }: { data: TIssue }) => { export const IssueGanttBlock = ({ data, textDisplacement }: { data: TIssue; textDisplacement: number }) => {
// hooks // store hooks
const { const {
router: { workspaceSlug }, router: { workspaceSlug },
} = useApplication(); } = useApplication();
@ -43,7 +44,15 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
} }
position="top-left" position="top-left"
> >
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{data?.name}</div> <div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100 overflow-hidden">
<span
style={{
...(textDisplacement > 0 ? { paddingLeft: `${textDisplacement}px` } : {}),
}}
>
{data?.name}
</span>
</div>
</Tooltip> </Tooltip>
</div> </div>
); );