mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: gantt sidebar and main content scroll sync
This commit is contained in:
parent
3ef0570f6a
commit
5ca137fb92
@ -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,18 +112,15 @@ 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 },
|
||||
{
|
||||
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} />}
|
||||
{isBlockVisibleOnChart ? (
|
||||
<ChartDraggable
|
||||
block={block}
|
||||
blockToRender={blockToRender}
|
||||
@ -111,6 +129,9 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
/>
|
||||
) : (
|
||||
<ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@ -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,8 +300,15 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||
<h6>Duration</h6>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
id="scroll-container"
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 && (
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user