plane/apps/app/components/gantt-chart/chart/index.tsx
Aaryan Khandelwal 47abe9db5e
dev: gantt chart revamp (#1900)
* style: gantt chart polishing

* chore: sidebar y-axis drag and drop

* chore: remove y-axis drag and drop from the main content

* refactor: drop end function

* refactor: resizing logic

* chore: x-axis block move

* chore: x-axis block move flag

* chore: update scroll end logic

* style: modules gantt chart

* style: block background tint

* refactor: context dispatcher types

* refactor: draggable component

* chore: filters added to gantt chart

* refactor: folder structure

* style: cycle blocks

* chore: move to block arrow

* chore: move to block on the right side arrow

* chore: added proper comments for functions

* refactor: blocks render logic

* fix: x-axis drag and drop

* chore: minor ui fixes

* chore: remove link tag from blocks

---------

Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-28 13:25:47 +05:30

338 lines
12 KiB
TypeScript

import { FC, useEffect, useState } from "react";
// icons
import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/20/solid";
// components
import { GanttChartBlocks } from "components/gantt-chart";
import { GanttSidebar } from "../sidebar";
// import { HourChartView } from "./hours";
// import { DayChartView } from "./day";
// import { WeekChartView } from "./week";
// import { BiWeekChartView } from "./bi-week";
import { MonthChartView } from "./month";
// import { QuarterChartView } from "./quarter";
// import { YearChartView } from "./year";
// views
import {
// generateHourChart,
// generateDayChart,
generateWeekChart,
generateBiWeekChart,
generateMonthChart,
generateQuarterChart,
generateYearChart,
getNumberOfDaysBetweenTwoDatesInMonth,
getNumberOfDaysBetweenTwoDatesInQuarter,
getNumberOfDaysBetweenTwoDatesInYear,
getMonthChartItemPositionWidthInMonth,
} from "../views";
// types
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
// data
import { currentViewDataWithView } from "../data";
// context
import { useChart } from "../hooks";
type ChartViewRootProps = {
border: boolean;
title: string;
loaderTitle: string;
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
SidebarBlockRender: React.FC<any>;
BlockRender: React.FC<any>;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableReorder: boolean;
bottomSpacing: boolean;
};
export const ChartViewRoot: FC<ChartViewRootProps> = ({
border,
title,
blocks = null,
loaderTitle,
blockUpdateHandler,
SidebarBlockRender,
BlockRender,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableReorder,
bottomSpacing,
}) => {
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
// blocks state management starts
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } =
useChart();
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
blocks && blocks.length > 0
? blocks.map((block: any) => ({
...block,
position: getMonthChartItemPositionWidthInMonth(view, block),
}))
: [];
useEffect(() => {
if (currentViewData && blocks)
setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
}, [currentViewData, blocks]);
// blocks state management ends
const handleChartView = (key: TGanttViews) => updateCurrentViewRenderPayload(null, key);
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
const selectedCurrentView: TGanttViews = view;
const selectedCurrentViewData: ChartDataType | undefined =
selectedCurrentView && selectedCurrentView === currentViewData?.key
? currentViewData
: currentViewDataWithView(view);
if (selectedCurrentViewData === undefined) return;
let currentRender: any;
// if (view === "hours") currentRender = generateHourChart(selectedCurrentViewData, side);
// if (view === "day") currentRender = generateDayChart(selectedCurrentViewData, side);
// if (view === "week") currentRender = generateWeekChart(selectedCurrentViewData, side);
// if (view === "bi_week") currentRender = generateBiWeekChart(selectedCurrentViewData, side);
if (selectedCurrentView === "month")
currentRender = generateMonthChart(selectedCurrentViewData, side);
// if (view === "quarter") currentRender = generateQuarterChart(selectedCurrentViewData, side);
// if (selectedCurrentView === "year")
// currentRender = generateYearChart(selectedCurrentViewData, side);
// updating the prevData, currentData and nextData
if (currentRender.payload.length > 0) {
if (side === "left") {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: selectedCurrentView,
currentViewData: currentRender.state,
renderView: [...currentRender.payload, ...renderView],
},
});
updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else if (side === "right") {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...renderView, ...currentRender.payload],
},
});
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...currentRender.payload],
},
});
setItemsContainerWidth(currentRender.scrollWidth);
setTimeout(() => {
handleScrollToCurrentSelectedDate(
currentRender.state,
currentRender.state.data.currentDate
);
}, 50);
}
}
};
const handleToday = () => updateCurrentViewRenderPayload(null, currentView);
// handling the scroll positioning from left and right
useEffect(() => {
handleToday();
}, []);
const updatingCurrentLeftScrollPosition = (width: number) => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
if (!scrollContainer) return;
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
};
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
const clientVisibleWidth: number = scrollContainer?.clientWidth;
let scrollWidth: number = 0;
let daysDifference: number = 0;
// if (currentView === "hours")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "day")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "week")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "bi_week")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
if (currentView === "month")
daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "quarter")
// daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date);
// if (currentView === "year")
// daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date);
scrollWidth =
daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width);
scrollContainer.scrollLeft = scrollWidth;
};
// handling scroll functionality
const onScroll = () => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
const scrollWidth: number = scrollContainer?.scrollWidth;
const clientVisibleWidth: number = scrollContainer?.clientWidth;
const currentScrollPosition: number = scrollContainer?.scrollLeft;
updateScrollLeft(currentScrollPosition);
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);
};
return (
<div
className={`${
fullScreenMode
? `fixed top-0 bottom-0 left-0 right-0 z-[999999] bg-custom-background-100`
: `relative`
} ${
border ? `border border-custom-border-200` : ``
} flex h-full flex-col rounded-sm select-none bg-custom-background-100 shadow`}
>
{/* chart header */}
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2">
{title && (
<div className="text-lg font-medium flex gap-2 items-center">
<div>{title}</div>
{/* <div className="text-xs rounded-full px-2 py-1 font-bold border border-custom-primary/75 bg-custom-primary/5 text-custom-text-100">
Gantt View Beta
</div> */}
</div>
)}
<div className="ml-auto">
{blocks === null ? (
<div className="text-sm font-medium ml-auto">Loading...</div>
) : (
<div className="text-sm font-medium ml-auto">
{blocks.length} {loaderTitle}
</div>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
{allViews &&
allViews.length > 0 &&
allViews.map((_chatView: any, _idx: any) => (
<div
key={_chatView?.key}
className={`cursor-pointer rounded-sm p-1 px-2 text-xs ${
currentView === _chatView?.key
? `bg-custom-background-80`
: `hover:bg-custom-background-90`
}`}
onClick={() => handleChartView(_chatView?.key)}
>
{_chatView?.title}
</div>
))}
</div>
<div className="flex items-center gap-1">
<div
className="cursor-pointer rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80"
onClick={handleToday}
>
Today
</div>
</div>
<div
className="transition-all border border-custom-border-200 p-1 flex justify-center items-center cursor-pointer rounded-sm hover:bg-custom-background-80"
onClick={() => setFullScreenMode((prevData) => !prevData)}
>
{fullScreenMode ? (
<ArrowsPointingInIcon className="h-4 w-4" />
) : (
<ArrowsPointingOutIcon className="h-4 w-4" />
)}
</div>
</div>
{/* content */}
<div
id="gantt-container"
className={`relative flex h-full w-full flex-1 overflow-hidden border-t border-custom-border-200 ${
bottomSpacing ? "mb-8" : ""
}`}
>
<div
id="gantt-sidebar"
className="h-full w-1/4 flex flex-col border-r border-custom-border-200 space-y-3"
>
<div className="h-[60px] border-b border-custom-border-200 box-border flex-shrink-0" />
<GanttSidebar
title={title}
blockUpdateHandler={blockUpdateHandler}
blocks={chartBlocks}
SidebarBlockRender={SidebarBlockRender}
enableReorder={enableReorder}
/>
</div>
<div
className="relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto horizontal-scroll-enable"
id="scroll-container"
onScroll={onScroll}
>
{/* {currentView && currentView === "hours" && <HourChartView />} */}
{/* {currentView && currentView === "day" && <DayChartView />} */}
{/* {currentView && currentView === "week" && <WeekChartView />} */}
{/* {currentView && currentView === "bi_week" && <BiWeekChartView />} */}
{currentView && currentView === "month" && <MonthChartView />}
{/* {currentView && currentView === "quarter" && <QuarterChartView />} */}
{/* {currentView && currentView === "year" && <YearChartView />} */}
{/* blocks */}
{currentView && currentViewData && (
<GanttChartBlocks
itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks}
BlockRender={BlockRender}
blockUpdateHandler={blockUpdateHandler}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
)}
</div>
</div>
</div>
);
};