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
import { useIssueDetail } from "hooks/store";
import { useChart } from "../hooks";
@ -12,7 +12,7 @@ import { IBlockUpdateData, IGanttBlock } from "../types";
export type GanttChartBlocksProps = {
itemsContainerWidth: number;
blocks: IGanttBlock[] | null;
blockToRender: (data: any) => React.ReactNode;
blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
@ -31,9 +31,12 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
enableBlockMove,
showAllBlocks,
} = props;
const { activeBlock, dispatch } = useChart();
// refs
const blocksContainerRef = useRef<HTMLDivElement>(null);
// store hooks
const { peekIssue } = useIssueDetail();
// chart hook
const { activeBlock, dispatch, scrollTop, updateScrollTop } = useChart();
// update the active block on hover
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 (
<div
ref={blocksContainerRef}
className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto"
style={{ width: `${itemsContainerWidth}px` }}
onScroll={handleBlocksScroll}
>
{blocks &&
blocks.length > 0 &&
@ -91,26 +112,26 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
return (
<div
key={`block-${block.id}`}
className={cn(
"h-11",
{ "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,
}
)}
className={cn("relative h-11", {
"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,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
>
{!isBlockVisibleOnChart && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />}
<ChartDraggable
block={block}
blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
{isBlockVisibleOnChart ? (
<ChartDraggable
block={block}
blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
) : (
<ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
)}
</div>
);
})}

View File

@ -1,4 +1,4 @@
import { FC, useEffect, useState } from "react";
import { FC, useEffect, useRef, useState } from "react";
// icons
// components
import { GanttChartBlocks } from "components/gantt-chart";
@ -39,7 +39,7 @@ type ChartViewRootProps = {
loaderTitle: string;
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode;
blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
@ -69,8 +69,11 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null); // blocks state management starts
// refs
const sidebarRef = useRef<HTMLDivElement>(null);
// 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) =>
blocks && blocks.length > 0
@ -202,17 +205,25 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
const scrollWidth: number = scrollContainer?.scrollWidth;
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 approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth);
if (currentScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView);
if (currentScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView);
if (currentLeftScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", 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 (
<div
className={`${
@ -289,7 +300,14 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
<h6>Duration</h6>
</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
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";
export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
// states;
const [state, dispatch] = useState<ChartContextData>({
currentView: initialView,
currentViewData: currentViewDataWithView(initialView),
@ -31,23 +32,23 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
allViews: allViewsWithData,
activeBlock: null,
});
const [scrollLeft, setScrollLeft] = useState(0);
const [scrollTop, setScrollTop] = useState(0);
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
const newState = chartReducer(state, action);
dispatch(() => newState);
return newState;
};
const updateScrollLeft = (scrollLeft: number) => {
setScrollLeft(scrollLeft);
};
const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft);
const updateScrollTop = (scrollTop: number) => setScrollTop(scrollTop);
return (
<ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}>
<ChartContext.Provider
value={{ ...state, scrollLeft, updateScrollLeft, scrollTop, updateScrollTop, dispatch: handleDispatch }}
>
{children}
</ChartContext.Provider>
);

View File

@ -7,7 +7,7 @@ import { IGanttBlock } from "../types";
type Props = {
block: IGanttBlock;
blockToRender: (data: any) => React.ReactNode;
blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right" | "move") => void;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
@ -223,6 +223,8 @@ export const ChartDraggable: React.FC<Props> = (props) => {
scrollLeft > block.position.marginLeft + block.position.width;
const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth;
const textDisplacement = scrollLeft - (block.position?.marginLeft ?? 0);
return (
<>
{/* 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" : ""}`}
onMouseDown={handleBlockMove}
>
{blockToRender(block.data)}
{blockToRender(block.data, textDisplacement)}
</div>
{/* right resize drag handle */}
{enableBlockRightResize && (

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,14 @@
// hooks
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
// ui
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { TIssue } from "@plane/types";
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
// hooks
export const IssueGanttBlock = ({ data, textDisplacement }: { data: TIssue; textDisplacement: number }) => {
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
@ -43,7 +44,15 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
}
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>
</div>
);