mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
refactor: gantt chart components
This commit is contained in:
parent
fb46d50937
commit
4943eb397e
@ -20,7 +20,7 @@ export type GanttChartBlocksProps = {
|
||||
showAllBlocks: boolean;
|
||||
};
|
||||
|
||||
export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
|
||||
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
||||
const {
|
||||
itemsContainerWidth,
|
||||
blocks,
|
@ -1 +1 @@
|
||||
export * from "./blocks-display";
|
||||
export * from "./blocks-list";
|
||||
|
64
web/components/gantt-chart/chart/header.tsx
Normal file
64
web/components/gantt-chart/chart/header.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
// hooks
|
||||
import { Expand, Shrink } from "lucide-react";
|
||||
import { useChart } from "../hooks";
|
||||
// types
|
||||
import { IGanttBlock, TGanttViews } from "../types";
|
||||
|
||||
type Props = {
|
||||
blocks: IGanttBlock[] | null;
|
||||
fullScreenMode: boolean;
|
||||
handleChartView: (view: TGanttViews) => void;
|
||||
handleToday: () => void;
|
||||
loaderTitle: string;
|
||||
title: string;
|
||||
toggleFullScreenMode: () => void;
|
||||
};
|
||||
|
||||
export const GanttChartHeader: React.FC<Props> = (props) => {
|
||||
const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props;
|
||||
// chart hook
|
||||
const { currentView, allViews } = useChart();
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2 z-10">
|
||||
<div className="flex items-center gap-2 text-lg font-medium">{title}</div>
|
||||
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto text-sm font-medium">{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{allViews &&
|
||||
allViews.map((_chatView: 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">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80"
|
||||
onClick={handleToday}
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center rounded-sm border border-custom-border-200 p-1 transition-all hover:bg-custom-background-80"
|
||||
onClick={toggleFullScreenMode}
|
||||
>
|
||||
{fullScreenMode ? <Shrink className="h-4 w-4" /> : <Expand className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
4
web/components/gantt-chart/chart/index.ts
Normal file
4
web/components/gantt-chart/chart/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./views";
|
||||
export * from "./header";
|
||||
export * from "./main-content";
|
||||
export * from "./root";
|
@ -1,334 +0,0 @@
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
// icons
|
||||
// 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";
|
||||
// icons
|
||||
import { Expand, Shrink } from "lucide-react";
|
||||
// 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;
|
||||
blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
enableBlockLeftResize: boolean;
|
||||
enableBlockRightResize: boolean;
|
||||
enableBlockMove: boolean;
|
||||
enableReorder: boolean;
|
||||
bottomSpacing: boolean;
|
||||
showAllBlocks: boolean;
|
||||
};
|
||||
|
||||
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||
const {
|
||||
border,
|
||||
title,
|
||||
blocks = null,
|
||||
loaderTitle,
|
||||
blockUpdateHandler,
|
||||
sidebarToRender,
|
||||
blockToRender,
|
||||
enableBlockLeftResize,
|
||||
enableBlockRightResize,
|
||||
enableBlockMove,
|
||||
enableReorder,
|
||||
bottomSpacing,
|
||||
showAllBlocks,
|
||||
} = props;
|
||||
// states
|
||||
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, updateScrollTop } =
|
||||
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();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const scrollContainer = document.getElementById("scroll-container") as HTMLDivElement;
|
||||
|
||||
const updatingCurrentLeftScrollPosition = (width: number) => {
|
||||
if (!scrollContainer) return;
|
||||
|
||||
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
|
||||
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
|
||||
};
|
||||
|
||||
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
|
||||
if (!scrollContainer) return;
|
||||
|
||||
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 = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||
const { clientWidth: clientVisibleWidth, scrollLeft: currentLeftScrollPosition, scrollWidth } = e.currentTarget;
|
||||
|
||||
updateScrollLeft(currentLeftScrollPosition);
|
||||
|
||||
const approxRangeLeft = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
|
||||
const approxRangeRight = scrollWidth - (approxRangeLeft + clientVisibleWidth);
|
||||
|
||||
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={`${
|
||||
fullScreenMode ? `fixed bottom-0 left-0 right-0 top-0 z-[999999] bg-custom-background-100` : `relative`
|
||||
} ${
|
||||
border ? `border border-custom-border-200` : ``
|
||||
} flex h-full select-none flex-col rounded-sm 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="flex items-center gap-2 text-lg font-medium">
|
||||
<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="ml-auto text-sm font-medium">Loading...</div>
|
||||
) : (
|
||||
<div className="ml-auto text-sm font-medium">
|
||||
{blocks.length} {loaderTitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{allViews &&
|
||||
allViews.length > 0 &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
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="flex cursor-pointer items-center justify-center rounded-sm border border-custom-border-200 p-1 transition-all hover:bg-custom-background-80"
|
||||
onClick={() => setFullScreenMode((prevData) => !prevData)}
|
||||
>
|
||||
{fullScreenMode ? <Shrink className="h-4 w-4" /> : <Expand 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="flex h-full w-1/4 flex-col border-r border-custom-border-200">
|
||||
<div className="box-border flex h-[60px] flex-shrink-0 items-end justify-between gap-2 border-b border-custom-border-200 pb-2 pl-10 pr-4 text-sm font-medium text-custom-text-300">
|
||||
<h6>{title}</h6>
|
||||
<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"
|
||||
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}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
showAllBlocks={showAllBlocks}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
139
web/components/gantt-chart/chart/main-content.tsx
Normal file
139
web/components/gantt-chart/chart/main-content.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import { useRef } from "react";
|
||||
// components
|
||||
import {
|
||||
BiWeekChartView,
|
||||
DayChartView,
|
||||
GanttChartBlocksList,
|
||||
HourChartView,
|
||||
IBlockUpdateData,
|
||||
IGanttBlock,
|
||||
MonthChartView,
|
||||
QuarterChartView,
|
||||
TGanttViews,
|
||||
WeekChartView,
|
||||
YearChartView,
|
||||
useChart,
|
||||
} from "components/gantt-chart";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
blocks: IGanttBlock[] | null;
|
||||
blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
bottomSpacing: boolean;
|
||||
chartBlocks: IGanttBlock[] | null;
|
||||
enableBlockLeftResize: boolean;
|
||||
enableBlockMove: boolean;
|
||||
enableBlockRightResize: boolean;
|
||||
enableReorder: boolean;
|
||||
itemsContainerWidth: number;
|
||||
showAllBlocks: boolean;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
title: string;
|
||||
updateCurrentViewRenderPayload: (direction: "left" | "right", currentView: TGanttViews) => void;
|
||||
};
|
||||
|
||||
export const GanttChartMainContent: React.FC<Props> = (props) => {
|
||||
const {
|
||||
blocks,
|
||||
blockToRender,
|
||||
blockUpdateHandler,
|
||||
bottomSpacing,
|
||||
chartBlocks,
|
||||
enableBlockLeftResize,
|
||||
enableBlockMove,
|
||||
enableBlockRightResize,
|
||||
enableReorder,
|
||||
itemsContainerWidth,
|
||||
showAllBlocks,
|
||||
sidebarToRender,
|
||||
title,
|
||||
updateCurrentViewRenderPayload,
|
||||
} = props;
|
||||
// refs
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
// chart hook
|
||||
const { currentView, currentViewData, updateScrollLeft, updateScrollTop } = useChart();
|
||||
|
||||
// handling scroll functionality
|
||||
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||
const { clientWidth: clientVisibleWidth, scrollLeft: currentLeftScrollPosition, scrollWidth } = e.currentTarget;
|
||||
|
||||
updateScrollLeft(currentLeftScrollPosition);
|
||||
|
||||
const approxRangeLeft = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
|
||||
const approxRangeRight = scrollWidth - (approxRangeLeft + clientVisibleWidth);
|
||||
|
||||
if (currentLeftScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView);
|
||||
if (currentLeftScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView);
|
||||
};
|
||||
|
||||
const onSidebarScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => updateScrollTop(e.currentTarget.scrollTop);
|
||||
|
||||
const CHART_VIEW_COMPONENTS: {
|
||||
[key in TGanttViews]: React.FC;
|
||||
} = {
|
||||
hours: HourChartView,
|
||||
day: DayChartView,
|
||||
week: WeekChartView,
|
||||
bi_week: BiWeekChartView,
|
||||
month: MonthChartView,
|
||||
quarter: QuarterChartView,
|
||||
year: YearChartView,
|
||||
};
|
||||
const ActiveChartView = CHART_VIEW_COMPONENTS[currentView];
|
||||
|
||||
return (
|
||||
<div
|
||||
// DO NOT REMOVE THE ID
|
||||
id="gantt-container"
|
||||
className={cn("relative flex h-full w-full flex-1 overflow-hidden border-t border-custom-border-200", {
|
||||
"mb-8": bottomSpacing,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
// DO NOT REMOVE THE ID
|
||||
id="gantt-sidebar"
|
||||
className="flex h-full w-1/4 flex-col border-r border-custom-border-200"
|
||||
>
|
||||
<div className="box-border flex h-[60px] flex-shrink-0 items-end justify-between gap-2 border-b border-custom-border-200 pb-2 pl-10 pr-4 text-sm font-medium text-custom-text-300">
|
||||
<h6>{title}</h6>
|
||||
<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>
|
||||
{currentView && (
|
||||
<div
|
||||
// DO NOT REMOVE THE ID
|
||||
id="scroll-container"
|
||||
className="relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto horizontal-scroll-enable"
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<ActiveChartView />
|
||||
{/* blocks */}
|
||||
{currentViewData && (
|
||||
<GanttChartBlocksList
|
||||
itemsContainerWidth={itemsContainerWidth}
|
||||
blocks={chartBlocks}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
showAllBlocks={showAllBlocks}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,72 +0,0 @@
|
||||
import { FC } from "react";
|
||||
// hooks
|
||||
import { useChart } from "../hooks";
|
||||
// types
|
||||
import { IMonthBlock } from "../views";
|
||||
|
||||
export const MonthChartView: FC<any> = () => {
|
||||
const { currentViewData, renderView } = useChart();
|
||||
|
||||
const monthBlocks: IMonthBlock[] = renderView;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-100/50">
|
||||
{monthBlocks &&
|
||||
monthBlocks.length > 0 &&
|
||||
monthBlocks.map((block, _idxRoot) => (
|
||||
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
|
||||
<div className="h-[60px] w-full">
|
||||
<div className="relative h-[30px]">
|
||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-3 py-2 text-xs font-medium capitalize">
|
||||
{block?.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-[30px] w-full">
|
||||
{block?.children &&
|
||||
block?.children.length > 0 &&
|
||||
block?.children.map((monthDay, _idx) => (
|
||||
<div
|
||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||
className="flex-shrink-0 border-b border-custom-border-200 py-1 text-center capitalize"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
<div className="space-x-1 text-xs">
|
||||
<span className="text-custom-text-200">{monthDay.dayData.shortTitle[0]}</span>{" "}
|
||||
<span className={monthDay.today ? "rounded-full bg-custom-primary-100 px-1 text-white" : ""}>
|
||||
{monthDay.day}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-100/50">
|
||||
{block?.children &&
|
||||
block?.children.length > 0 &&
|
||||
block?.children.map((monthDay, _idx) => (
|
||||
<div
|
||||
key={`column-${_idxRoot}-${_idx}`}
|
||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
<div
|
||||
className={`relative flex h-full w-full flex-1 justify-center ${
|
||||
["sat", "sun"].includes(monthDay?.dayData?.shortTitle || "") ? `bg-custom-background-90` : ``
|
||||
}`}
|
||||
>
|
||||
{/* {monthDay?.today && (
|
||||
<div className="absolute top-0 bottom-0 w-[1px] bg-red-500" />
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
216
web/components/gantt-chart/chart/root.tsx
Normal file
216
web/components/gantt-chart/chart/root.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
// components
|
||||
import { GanttChartHeader, useChart, GanttChartMainContent } from "components/gantt-chart";
|
||||
// views
|
||||
import {
|
||||
// generateHourChart,
|
||||
// generateDayChart,
|
||||
// generateWeekChart,
|
||||
// generateBiWeekChart,
|
||||
generateMonthChart,
|
||||
// generateQuarterChart,
|
||||
// generateYearChart,
|
||||
getNumberOfDaysBetweenTwoDatesInMonth,
|
||||
// getNumberOfDaysBetweenTwoDatesInQuarter,
|
||||
// getNumberOfDaysBetweenTwoDatesInYear,
|
||||
getMonthChartItemPositionWidthInMonth,
|
||||
} from "../views";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
|
||||
// data
|
||||
import { currentViewDataWithView } from "../data";
|
||||
|
||||
type ChartViewRootProps = {
|
||||
border: boolean;
|
||||
title: string;
|
||||
loaderTitle: string;
|
||||
blocks: IGanttBlock[] | null;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
enableBlockLeftResize: boolean;
|
||||
enableBlockRightResize: boolean;
|
||||
enableBlockMove: boolean;
|
||||
enableReorder: boolean;
|
||||
bottomSpacing: boolean;
|
||||
showAllBlocks: boolean;
|
||||
};
|
||||
|
||||
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||
const {
|
||||
border,
|
||||
title,
|
||||
blocks = null,
|
||||
loaderTitle,
|
||||
blockUpdateHandler,
|
||||
sidebarToRender,
|
||||
blockToRender,
|
||||
enableBlockLeftResize,
|
||||
enableBlockRightResize,
|
||||
enableBlockMove,
|
||||
enableReorder,
|
||||
bottomSpacing,
|
||||
showAllBlocks,
|
||||
} = props;
|
||||
// states
|
||||
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
|
||||
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
|
||||
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
|
||||
// hooks
|
||||
const { currentView, currentViewData, renderView, dispatch } = useChart();
|
||||
|
||||
// rendering the block structure
|
||||
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
|
||||
blocks && blocks.length > 0
|
||||
? blocks.map((block: any) => ({
|
||||
...block,
|
||||
position: getMonthChartItemPositionWidthInMonth(view, block),
|
||||
}))
|
||||
: [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentViewData || !blocks) return;
|
||||
setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
|
||||
}, [currentViewData, blocks]);
|
||||
|
||||
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();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const scrollContainer = document.getElementById("scroll-container") as HTMLDivElement;
|
||||
|
||||
const updatingCurrentLeftScrollPosition = (width: number) => {
|
||||
if (!scrollContainer) return;
|
||||
|
||||
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
|
||||
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
|
||||
};
|
||||
|
||||
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
|
||||
if (!scrollContainer) return;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative flex flex-col h-full select-none rounded-sm bg-custom-background-100 shadow", {
|
||||
"fixed inset-0 z-[999999] bg-custom-background-100": fullScreenMode,
|
||||
"border border-custom-border-200": border,
|
||||
})}
|
||||
>
|
||||
<GanttChartHeader
|
||||
blocks={blocks}
|
||||
fullScreenMode={fullScreenMode}
|
||||
toggleFullScreenMode={() => setFullScreenMode((prevData) => !prevData)}
|
||||
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
|
||||
handleToday={handleToday}
|
||||
loaderTitle={loaderTitle}
|
||||
title={title}
|
||||
/>
|
||||
<GanttChartMainContent
|
||||
blocks={blocks}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
bottomSpacing={bottomSpacing}
|
||||
chartBlocks={chartBlocks}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableReorder={enableReorder}
|
||||
itemsContainerWidth={itemsContainerWidth}
|
||||
showAllBlocks={showAllBlocks}
|
||||
sidebarToRender={sidebarToRender}
|
||||
title={title}
|
||||
updateCurrentViewRenderPayload={updateCurrentViewRenderPayload}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "../hooks";
|
||||
import { useChart } from "components/gantt-chart";
|
||||
|
||||
export const BiWeekChartView: FC<any> = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
@ -1,6 +1,6 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "../hooks";
|
||||
import { useChart } from "../../hooks";
|
||||
|
||||
export const DayChartView: FC<any> = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
@ -1,6 +1,6 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "../hooks";
|
||||
import { useChart } from "components/gantt-chart";
|
||||
|
||||
export const HourChartView: FC<any> = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
7
web/components/gantt-chart/chart/views/index.ts
Normal file
7
web/components/gantt-chart/chart/views/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from "./bi-week";
|
||||
export * from "./day";
|
||||
export * from "./hours";
|
||||
export * from "./month";
|
||||
export * from "./quarter";
|
||||
export * from "./week";
|
||||
export * from "./year";
|
70
web/components/gantt-chart/chart/views/month.tsx
Normal file
70
web/components/gantt-chart/chart/views/month.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { FC } from "react";
|
||||
// hooks
|
||||
import { useChart } from "components/gantt-chart";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
import { IMonthBlock } from "../../views";
|
||||
|
||||
export const MonthChartView: FC<any> = () => {
|
||||
// chart hook
|
||||
const { currentViewData, renderView } = useChart();
|
||||
|
||||
const monthBlocks: IMonthBlock[] = renderView;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-100/50">
|
||||
{monthBlocks?.map((block, rootIndex) => (
|
||||
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
|
||||
<div className="h-[60px] w-full">
|
||||
<div className="relative h-[30px]">
|
||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-3 py-2 text-xs font-medium capitalize">
|
||||
{block?.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-[30px] w-full">
|
||||
{block?.children?.map((monthDay, _idx) => (
|
||||
<div
|
||||
key={`sub-title-${rootIndex}-${_idx}`}
|
||||
className="flex-shrink-0 border-b border-custom-border-200 py-1 text-center capitalize"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
<div className="space-x-1 text-xs">
|
||||
<span className="text-custom-text-200">{monthDay.dayData.shortTitle[0]}</span>{" "}
|
||||
<span className={monthDay.today ? "rounded-full bg-custom-primary-100 px-1 text-white" : ""}>
|
||||
{monthDay.day}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-100/50">
|
||||
{block?.children?.map((monthDay, index) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
<div
|
||||
className={cn("relative flex h-full w-full flex-1 justify-center", {
|
||||
"bg-custom-background-90": ["sat", "sun"].includes(monthDay?.dayData?.shortTitle),
|
||||
})}
|
||||
>
|
||||
{/* highlight today */}
|
||||
{/* {monthDay?.today && (
|
||||
<div className="absolute top-0 bottom-0 w-[1px] bg-red-500" />
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "../hooks";
|
||||
import { useChart } from "../../hooks";
|
||||
|
||||
export const QuarterChartView: FC<any> = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
@ -1,6 +1,6 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "../hooks";
|
||||
import { useChart } from "../../hooks";
|
||||
|
||||
export const WeekChartView: FC<any> = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
@ -1,6 +1,6 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "../hooks";
|
||||
import { useChart } from "../../hooks";
|
||||
|
||||
export const YearChartView: FC<any> = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
@ -212,7 +212,9 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
||||
|
||||
if (!block) return;
|
||||
|
||||
setPosFromLeft(block.getBoundingClientRect().left);
|
||||
requestAnimationFrame(() => {
|
||||
setPosFromLeft(block.getBoundingClientRect().left);
|
||||
});
|
||||
}, [scrollLeft]);
|
||||
// check if block is hidden on either side
|
||||
const isBlockHiddenOnLeft =
|
||||
@ -238,17 +240,17 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
{/* move to right side hidden block button */}
|
||||
{isBlockHiddenOnRight && (
|
||||
<div
|
||||
className="absolute top-1/2 z-[1] grid h-8 w-8 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={handleScrollToBlock}
|
||||
style={{
|
||||
transform: `translate(${(block.position?.marginLeft ?? 0) - posFromLeft + window.innerWidth - 36}px, -50%)`,
|
||||
}}
|
||||
>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
)}
|
||||
{/* {isBlockHiddenOnRight && ( */}
|
||||
<div
|
||||
className="fixed z-0 right-1 grid h-8 w-8 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={handleScrollToBlock}
|
||||
style={{
|
||||
top: `${(resizableRef.current?.getBoundingClientRect().top ?? 0) + 6}px`,
|
||||
}}
|
||||
>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
{/* )} */}
|
||||
<div
|
||||
id={`block-${block.id}`}
|
||||
ref={resizableRef}
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from "./blocks";
|
||||
export * from "./chart";
|
||||
export * from "./helpers";
|
||||
export * from "./hooks";
|
||||
export * from "./root";
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { FC } from "react";
|
||||
// components
|
||||
import { ChartViewRoot } from "./chart";
|
||||
import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
|
||||
// context
|
||||
import { ChartContextProvider } from "./contexts";
|
||||
// types
|
||||
import { IBlockUpdateData, IGanttBlock } from "./types";
|
||||
|
||||
type GanttChartRootProps = {
|
||||
border?: boolean;
|
||||
|
Loading…
Reference in New Issue
Block a user