[WEB-306] fix: Gantt chart bugs, refactor Gantt context (#3775)

* chore: initialize gantt layout store

* fix: modules being refetched on creation

* fix: scrollLeft calculation logic

* chore: modules list item dropdown position

* refactor: active block logic

* refactor: main content block component

* chore: remove unnecessary conditions for duration
This commit is contained in:
Aaryan Khandelwal 2024-02-23 19:10:45 +05:30 committed by GitHub
parent 46e1d46005
commit ba6479674c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 934 additions and 866 deletions

View File

@ -14,7 +14,7 @@ import {
IssueListItemProps, IssueListItemProps,
} from "components/dashboard/widgets"; } from "components/dashboard/widgets";
// ui // ui
import { getButtonStyling } from "@plane/ui"; import { Loader, getButtonStyling } from "@plane/ui";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { getRedirectionFilters } from "helpers/dashboard.helper"; import { getRedirectionFilters } from "helpers/dashboard.helper";
@ -63,7 +63,12 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
<> <>
<div className="h-full"> <div className="h-full">
{isLoading ? ( {isLoading ? (
<></> <Loader className="space-y-4 mx-6 mt-7">
<Loader.Item height="25px" />
<Loader.Item height="25px" />
<Loader.Item height="25px" />
<Loader.Item height="25px" />
</Loader>
) : issues.length > 0 ? ( ) : issues.length > 0 ? (
<> <>
<div className="mt-7 mx-6 border-b-[0.5px] border-custom-border-200 grid grid-cols-6 gap-1 text-xs text-custom-text-300 pb-1"> <div className="mt-7 mx-6 border-b-[0.5px] border-custom-border-200 grid grid-cols-6 gap-1 text-xs text-custom-text-300 pb-1">

View File

@ -0,0 +1,106 @@
import { observer } from "mobx-react";
// hooks
import { useGanttChart } from "../hooks";
import { useIssueDetail } from "hooks/store";
// components
import { ChartAddBlock, ChartDraggable } from "../helpers";
// helpers
import { cn } from "helpers/common.helper";
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "../types";
// constants
import { BLOCK_HEIGHT } from "../constants";
type Props = {
block: IGanttBlock;
blockToRender: (data: any) => React.ReactNode;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableAddBlock: boolean;
ganttContainerRef: React.RefObject<HTMLDivElement>;
};
export const GanttChartBlock: React.FC<Props> = observer((props) => {
const {
block,
blockToRender,
blockUpdateHandler,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableAddBlock,
ganttContainerRef,
} = props;
// store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart();
const { peekIssue } = useIssueDetail();
const isBlockVisibleOnChart = block.start_date && block.target_date;
const handleChartBlockPosition = (
block: IGanttBlock,
totalBlockShifts: number,
dragDirection: "left" | "right" | "move"
) => {
if (!block.start_date || !block.target_date) return;
const originalStartDate = new Date(block.start_date);
const updatedStartDate = new Date(originalStartDate);
const originalTargetDate = new Date(block.target_date);
const updatedTargetDate = new Date(originalTargetDate);
// update the start date on left resize
if (dragDirection === "left") updatedStartDate.setDate(originalStartDate.getDate() - totalBlockShifts);
// update the target date on right resize
else if (dragDirection === "right") updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts);
// update both the dates on x-axis move
else if (dragDirection === "move") {
updatedStartDate.setDate(originalStartDate.getDate() + totalBlockShifts);
updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts);
}
// call the block update handler with the updated dates
blockUpdateHandler(block.data, {
start_date: renderFormattedPayloadDate(updatedStartDate) ?? undefined,
target_date: renderFormattedPayloadDate(updatedTargetDate) ?? undefined,
});
};
return (
<div
key={`block-${block.id}`}
className="relative min-w-full w-max"
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
<div
className={cn("relative h-full", {
"bg-custom-background-80": isBlockActive(block.id),
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
>
{isBlockVisibleOnChart ? (
<ChartDraggable
block={block}
blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
ganttContainerRef={ganttContainerRef}
/>
) : (
enableAddBlock && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
)}
</div>
</div>
);
});

View File

@ -1,16 +1,10 @@
import { observer } from "mobx-react";
import { FC } from "react"; import { FC } from "react";
// hooks // components
import { useIssueDetail } from "hooks/store"; import { GanttChartBlock } from "./block";
import { useChart } from "../hooks";
// helpers
import { ChartAddBlock, ChartDraggable } from "components/gantt-chart";
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types // types
import { IBlockUpdateData, IGanttBlock } from "../types"; import { IBlockUpdateData, IGanttBlock } from "../types";
// constants // constants
import { BLOCK_HEIGHT, HEADER_HEIGHT } from "../constants"; import { HEADER_HEIGHT } from "../constants";
export type GanttChartBlocksProps = { export type GanttChartBlocksProps = {
itemsContainerWidth: number; itemsContainerWidth: number;
@ -21,10 +15,11 @@ export type GanttChartBlocksProps = {
enableBlockRightResize: boolean; enableBlockRightResize: boolean;
enableBlockMove: boolean; enableBlockMove: boolean;
enableAddBlock: boolean; enableAddBlock: boolean;
ganttContainerRef: React.RefObject<HTMLDivElement>;
showAllBlocks: boolean; showAllBlocks: boolean;
}; };
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = observer((props) => { export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
const { const {
itemsContainerWidth, itemsContainerWidth,
blocks, blocks,
@ -34,52 +29,9 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = observer((props)
enableBlockRightResize, enableBlockRightResize,
enableBlockMove, enableBlockMove,
enableAddBlock, enableAddBlock,
ganttContainerRef,
showAllBlocks, showAllBlocks,
} = props; } = props;
// store hooks
const { peekIssue } = useIssueDetail();
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
activeBlock: block,
},
});
};
const handleChartBlockPosition = (
block: IGanttBlock,
totalBlockShifts: number,
dragDirection: "left" | "right" | "move"
) => {
if (!block.start_date || !block.target_date) return;
const originalStartDate = new Date(block.start_date);
const updatedStartDate = new Date(originalStartDate);
const originalTargetDate = new Date(block.target_date);
const updatedTargetDate = new Date(originalTargetDate);
// update the start date on left resize
if (dragDirection === "left") updatedStartDate.setDate(originalStartDate.getDate() - totalBlockShifts);
// update the target date on right resize
else if (dragDirection === "right") updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts);
// update both the dates on x-axis move
else if (dragDirection === "move") {
updatedStartDate.setDate(originalStartDate.getDate() + totalBlockShifts);
updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts);
}
// call the block update handler with the updated dates
blockUpdateHandler(block.data, {
start_date: renderFormattedPayloadDate(updatedStartDate) ?? undefined,
target_date: renderFormattedPayloadDate(updatedTargetDate) ?? undefined,
});
};
return ( return (
<div <div
@ -93,41 +45,19 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = observer((props)
// hide the block if it doesn't have start and target dates and showAllBlocks is false // hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !(block.start_date && block.target_date)) return; if (!showAllBlocks && !(block.start_date && block.target_date)) return;
const isBlockVisibleOnChart = block.start_date && block.target_date;
return ( return (
<div <GanttChartBlock
key={`block-${block.id}`} block={block}
className="relative min-w-full w-max" blockToRender={blockToRender}
style={{ blockUpdateHandler={blockUpdateHandler}
height: `${BLOCK_HEIGHT}px`, enableBlockLeftResize={enableBlockLeftResize}
}} enableBlockRightResize={enableBlockRightResize}
> enableBlockMove={enableBlockMove}
<div enableAddBlock={enableAddBlock}
className={cn("relative h-full", { ganttContainerRef={ganttContainerRef}
"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 ? (
<ChartDraggable
block={block}
blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
) : (
enableAddBlock && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
)}
</div>
</div>
); );
})} })}
</div> </div>
); );
}); };

View File

@ -1,10 +1,13 @@
import { Expand, Shrink } from "lucide-react"; import { Expand, Shrink } from "lucide-react";
// hooks // hooks
import { useChart } from "../hooks";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// types // types
import { IGanttBlock, TGanttViews } from "../types"; import { IGanttBlock, TGanttViews } from "../types";
// constants
import { VIEWS_LIST } from "components/gantt-chart/data";
import { useGanttChart } from "../hooks/use-gantt-chart";
import { observer } from "mobx-react";
type Props = { type Props = {
blocks: IGanttBlock[] | null; blocks: IGanttBlock[] | null;
@ -16,10 +19,10 @@ type Props = {
toggleFullScreenMode: () => void; toggleFullScreenMode: () => void;
}; };
export const GanttChartHeader: React.FC<Props> = (props) => { export const GanttChartHeader: React.FC<Props> = observer((props) => {
const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props; const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props;
// chart hook // chart hook
const { currentView, allViews } = useChart(); const { currentView } = useGanttChart();
return ( 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="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2 z-10">
@ -29,7 +32,7 @@ export const GanttChartHeader: React.FC<Props> = (props) => {
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{allViews?.map((chartView: any) => ( {VIEWS_LIST.map((chartView: any) => (
<div <div
key={chartView?.key} key={chartView?.key}
className={cn("cursor-pointer rounded-sm p-1 px-2 text-xs", { className={cn("cursor-pointer rounded-sm p-1 px-2 text-xs", {
@ -56,4 +59,4 @@ export const GanttChartHeader: React.FC<Props> = (props) => {
</button> </button>
</div> </div>
); );
}; });

View File

@ -1,3 +1,7 @@
import { useRef } from "react";
import { observer } from "mobx-react";
// hooks
import { useGanttChart } from "../hooks/use-gantt-chart";
// components // components
import { import {
BiWeekChartView, BiWeekChartView,
@ -12,7 +16,6 @@ import {
TGanttViews, TGanttViews,
WeekChartView, WeekChartView,
YearChartView, YearChartView,
useChart,
} from "components/gantt-chart"; } from "components/gantt-chart";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
@ -36,7 +39,7 @@ type Props = {
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
}; };
export const GanttChartMainContent: React.FC<Props> = (props) => { export const GanttChartMainContent: React.FC<Props> = observer((props) => {
const { const {
blocks, blocks,
blockToRender, blockToRender,
@ -55,13 +58,15 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
updateCurrentViewRenderPayload, updateCurrentViewRenderPayload,
quickAdd, quickAdd,
} = props; } = props;
// refs
const ganttContainerRef = useRef<HTMLDivElement>(null);
// chart hook // chart hook
const { currentView, currentViewData, updateScrollLeft } = useChart(); const { currentView, currentViewData } = useGanttChart();
// handling scroll functionality // handling scroll functionality
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => { const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget; const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
updateScrollLeft(scrollLeft); // updateScrollLeft(scrollLeft);
const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth; const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth;
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth); const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
@ -95,6 +100,7 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
"mb-8": bottomSpacing, "mb-8": bottomSpacing,
} }
)} )}
ref={ganttContainerRef}
onScroll={onScroll} onScroll={onScroll}
> >
<GanttChartSidebar <GanttChartSidebar
@ -105,7 +111,7 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
title={title} title={title}
quickAdd={quickAdd} quickAdd={quickAdd}
/> />
<div className="relative min-h-full h-max flex-shrink-0 flex-grow"> <div className="relative h-full flex-shrink-0 flex-grow">
<ActiveChartView /> <ActiveChartView />
{currentViewData && ( {currentViewData && (
<GanttChartBlocksList <GanttChartBlocksList
@ -117,10 +123,11 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
enableBlockRightResize={enableBlockRightResize} enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove} enableBlockMove={enableBlockMove}
enableAddBlock={enableAddBlock} enableAddBlock={enableAddBlock}
ganttContainerRef={ganttContainerRef}
showAllBlocks={showAllBlocks} showAllBlocks={showAllBlocks}
/> />
)} )}
</div> </div>
</div> </div>
); );
}; });

View File

@ -1,6 +1,9 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
// hooks
import { useGanttChart } from "../hooks/use-gantt-chart";
// components // components
import { GanttChartHeader, useChart, GanttChartMainContent } from "components/gantt-chart"; import { GanttChartHeader, GanttChartMainContent } from "components/gantt-chart";
// views // views
import { import {
generateMonthChart, generateMonthChart,
@ -34,7 +37,7 @@ type ChartViewRootProps = {
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
}; };
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => { export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
const { const {
border, border,
title, title,
@ -57,7 +60,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
const [fullScreenMode, setFullScreenMode] = useState(false); const [fullScreenMode, setFullScreenMode] = useState(false);
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null); const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
// hooks // hooks
const { currentView, currentViewData, renderView, dispatch } = useChart(); const { currentView, currentViewData, renderView, updateCurrentView, updateCurrentViewData, updateRenderView } =
useGanttChart();
// rendering the block structure // rendering the block structure
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) => const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
@ -87,36 +91,20 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
// updating the prevData, currentData and nextData // updating the prevData, currentData and nextData
if (currentRender.payload.length > 0) { if (currentRender.payload.length > 0) {
updateCurrentViewData(currentRender.state);
if (side === "left") { if (side === "left") {
dispatch({ updateCurrentView(selectedCurrentView);
type: "PARTIAL_UPDATE", updateRenderView([...currentRender.payload, ...renderView]);
payload: {
currentView: selectedCurrentView,
currentViewData: currentRender.state,
renderView: [...currentRender.payload, ...renderView],
},
});
updatingCurrentLeftScrollPosition(currentRender.scrollWidth); updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else if (side === "right") { } else if (side === "right") {
dispatch({ updateCurrentView(view);
type: "PARTIAL_UPDATE", updateRenderView([...renderView, ...currentRender.payload]);
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...renderView, ...currentRender.payload],
},
});
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else { } else {
dispatch({ updateCurrentView(view);
type: "PARTIAL_UPDATE", updateRenderView(currentRender.payload);
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...currentRender.payload],
},
});
setItemsContainerWidth(currentRender.scrollWidth); setItemsContainerWidth(currentRender.scrollWidth);
setTimeout(() => { setTimeout(() => {
handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate); handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate);
@ -206,4 +194,4 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
/> />
</div> </div>
); );
}; });

View File

@ -1,10 +1,11 @@
import { FC } from "react"; import { FC } from "react";
// context import { observer } from "mobx-react";
import { useChart } from "components/gantt-chart"; // hooks
import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart";
export const BiWeekChartView: FC<any> = () => { export const BiWeekChartView: FC<any> = observer(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); const { currentView, currentViewData, renderView } = useGanttChart();
return ( return (
<> <>
@ -50,4 +51,4 @@ export const BiWeekChartView: FC<any> = () => {
</div> </div>
</> </>
); );
}; });

View File

@ -1,10 +1,11 @@
import { FC } from "react"; import { FC } from "react";
// context import { observer } from "mobx-react";
import { useChart } from "../../hooks"; // hooks
import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart";
export const DayChartView: FC<any> = () => { export const DayChartView: FC<any> = observer(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); const { currentView, currentViewData, renderView } = useGanttChart();
return ( return (
<> <>
@ -50,4 +51,4 @@ export const DayChartView: FC<any> = () => {
</div> </div>
</> </>
); );
}; });

View File

@ -1,10 +1,11 @@
import { FC } from "react"; import { FC } from "react";
// context import { observer } from "mobx-react";
import { useChart } from "components/gantt-chart"; // hooks
import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart";
export const HourChartView: FC<any> = () => { export const HourChartView: FC<any> = observer(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); const { currentView, currentViewData, renderView } = useGanttChart();
return ( return (
<> <>
@ -50,4 +51,4 @@ export const HourChartView: FC<any> = () => {
</div> </div>
</> </>
); );
}; });

View File

@ -1,6 +1,7 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react";
// hooks // hooks
import { useChart } from "components/gantt-chart"; import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// types // types
@ -8,9 +9,9 @@ import { IMonthBlock } from "../../views";
// constants // constants
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "components/gantt-chart/constants"; import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "components/gantt-chart/constants";
export const MonthChartView: FC<any> = () => { export const MonthChartView: FC<any> = observer(() => {
// chart hook // chart hook
const { currentViewData, renderView } = useChart(); const { currentViewData, renderView } = useGanttChart();
const monthBlocks: IMonthBlock[] = renderView; const monthBlocks: IMonthBlock[] = renderView;
return ( return (
@ -71,4 +72,4 @@ export const MonthChartView: FC<any> = () => {
))} ))}
</div> </div>
); );
}; });

View File

@ -1,10 +1,11 @@
import { FC } from "react"; import { FC } from "react";
// context import { observer } from "mobx-react";
import { useChart } from "../../hooks"; // hooks
import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart";
export const QuarterChartView: FC<any> = () => { export const QuarterChartView: FC<any> = observer(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); const { currentView, currentViewData, renderView } = useGanttChart();
return ( return (
<> <>
@ -46,4 +47,4 @@ export const QuarterChartView: FC<any> = () => {
</div> </div>
</> </>
); );
}; });

View File

@ -1,10 +1,11 @@
import { FC } from "react"; import { FC } from "react";
// context import { observer } from "mobx-react";
import { useChart } from "../../hooks"; // hooks
import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart";
export const WeekChartView: FC<any> = () => { export const WeekChartView: FC<any> = observer(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); const { currentView, currentViewData, renderView } = useGanttChart();
return ( return (
<> <>
@ -50,4 +51,4 @@ export const WeekChartView: FC<any> = () => {
</div> </div>
</> </>
); );
}; });

View File

@ -1,10 +1,11 @@
import { FC } from "react"; import { FC } from "react";
// context import { observer } from "mobx-react";
import { useChart } from "../../hooks"; // hooks
import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart";
export const YearChartView: FC<any> = () => { export const YearChartView: FC<any> = observer(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); const { currentView, currentViewData, renderView } = useGanttChart();
return ( return (
<> <>
@ -46,4 +47,4 @@ export const YearChartView: FC<any> = () => {
</div> </div>
</> </>
); );
}; });

View File

@ -1,57 +1,19 @@
import React, { createContext, useState } from "react"; import { createContext } from "react";
// types // mobx store
import { ChartContextData, ChartContextActionPayload, ChartContextReducer } from "../types"; import { GanttStore } from "store/issue/issue_gantt_view.store";
// data
import { allViewsWithData, currentViewDataWithView } from "../data";
export const ChartContext = createContext<ChartContextReducer | undefined>(undefined); let ganttViewStore = new GanttStore();
const chartReducer = (state: ChartContextData, action: ChartContextActionPayload): ChartContextData => { export const GanttStoreContext = createContext<GanttStore>(ganttViewStore);
switch (action.type) {
case "CURRENT_VIEW": const initializeStore = () => {
return { ...state, currentView: action.payload }; const _ganttStore = ganttViewStore ?? new GanttStore();
case "CURRENT_VIEW_DATA": if (typeof window === "undefined") return _ganttStore;
return { ...state, currentViewData: action.payload }; if (!ganttViewStore) ganttViewStore = _ganttStore;
case "RENDER_VIEW": return _ganttStore;
return { ...state, currentViewData: action.payload };
case "PARTIAL_UPDATE":
return { ...state, ...action.payload };
default:
return state;
}
}; };
const initialView = "month"; export const GanttStoreProvider = ({ children }: any) => {
const store = initializeStore();
export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { return <GanttStoreContext.Provider value={store}>{children}</GanttStoreContext.Provider>;
// states;
const [state, dispatch] = useState<ChartContextData>({
currentView: initialView,
currentViewData: currentViewDataWithView(initialView),
renderView: [],
allViews: allViewsWithData,
activeBlock: null,
});
const [scrollLeft, setScrollLeft] = useState(0);
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
const newState = chartReducer(state, action);
dispatch(() => newState);
return newState;
};
const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft);
return (
<ChartContext.Provider
value={{
...state,
scrollLeft,
updateScrollLeft,
dispatch: handleDispatch,
}}
>
{children}
</ChartContext.Provider>
);
}; };

View File

@ -1,5 +1,5 @@
// types // types
import { WeekMonthDataType, ChartDataType } from "../types"; import { WeekMonthDataType, ChartDataType, TGanttViews } from "../types";
// constants // constants
export const weeks: WeekMonthDataType[] = [ export const weeks: WeekMonthDataType[] = [
@ -53,7 +53,7 @@ export const datePreview = (date: Date, includeTime: boolean = false) => {
}; };
// context data // context data
export const allViewsWithData: ChartDataType[] = [ export const VIEWS_LIST: ChartDataType[] = [
// { // {
// key: "hours", // key: "hours",
// title: "Hours", // title: "Hours",
@ -133,7 +133,5 @@ export const allViewsWithData: ChartDataType[] = [
// }, // },
]; ];
export const currentViewDataWithView = (view: string = "month") => { export const currentViewDataWithView = (view: TGanttViews = "month") =>
const currentView: ChartDataType | undefined = allViewsWithData.find((_viewData) => _viewData.key === view); VIEWS_LIST.find((_viewData) => _viewData.key === view);
return currentView;
};

View File

@ -1,21 +1,21 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { addDays } from "date-fns"; import { addDays } from "date-fns";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// hooks
import { useChart } from "../hooks";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// helpers // helpers
import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types // types
import { IBlockUpdateData, IGanttBlock } from "../types"; import { IBlockUpdateData, IGanttBlock } from "../types";
import { useGanttChart } from "../hooks/use-gantt-chart";
import { observer } from "mobx-react";
type Props = { type Props = {
block: IGanttBlock; block: IGanttBlock;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
}; };
export const ChartAddBlock: React.FC<Props> = (props) => { export const ChartAddBlock: React.FC<Props> = observer((props) => {
const { block, blockUpdateHandler } = props; const { block, blockUpdateHandler } = props;
// states // states
const [isButtonVisible, setIsButtonVisible] = useState(false); const [isButtonVisible, setIsButtonVisible] = useState(false);
@ -24,7 +24,7 @@ export const ChartAddBlock: React.FC<Props> = (props) => {
// refs // refs
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
// chart hook // chart hook
const { currentViewData } = useChart(); const { currentViewData } = useGanttChart();
const handleButtonClick = () => { const handleButtonClick = () => {
if (!currentViewData) return; if (!currentViewData) return;
@ -88,4 +88,4 @@ export const ChartAddBlock: React.FC<Props> = (props) => {
)} )}
</div> </div>
); );
}; });

View File

@ -1,12 +0,0 @@
// types
import { TIssue } from "@plane/types";
import { IGanttBlock } from "components/gantt-chart";
export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] =>
blocks?.map((block) => ({
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: block.start_date ? new Date(block.start_date) : null,
target_date: block.target_date ? new Date(block.target_date) : null,
}));

View File

@ -1,11 +1,13 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
// hooks // hooks
import { IGanttBlock, useChart } from "components/gantt-chart"; import { IGanttBlock } from "components/gantt-chart";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// constants // constants
import { SIDEBAR_WIDTH } from "../constants"; import { SIDEBAR_WIDTH } from "../constants";
import { useGanttChart } from "../hooks/use-gantt-chart";
import { observer } from "mobx-react";
type Props = { type Props = {
block: IGanttBlock; block: IGanttBlock;
@ -14,19 +16,29 @@ type Props = {
enableBlockLeftResize: boolean; enableBlockLeftResize: boolean;
enableBlockRightResize: boolean; enableBlockRightResize: boolean;
enableBlockMove: boolean; enableBlockMove: boolean;
ganttContainerRef: React.RefObject<HTMLDivElement>;
}; };
export const ChartDraggable: React.FC<Props> = (props) => { export const ChartDraggable: React.FC<Props> = observer((props) => {
const { block, blockToRender, handleBlock, enableBlockLeftResize, enableBlockRightResize, enableBlockMove } = props; const {
block,
blockToRender,
handleBlock,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
ganttContainerRef,
} = props;
// states // states
const [isLeftResizing, setIsLeftResizing] = useState(false); const [isLeftResizing, setIsLeftResizing] = useState(false);
const [isRightResizing, setIsRightResizing] = useState(false); const [isRightResizing, setIsRightResizing] = useState(false);
const [isMoving, setIsMoving] = useState(false); const [isMoving, setIsMoving] = useState(false);
const [isHidden, setIsHidden] = useState(true); const [isHidden, setIsHidden] = useState(true);
const [scrollLeft, setScrollLeft] = useState(0);
// refs // refs
const resizableRef = useRef<HTMLDivElement>(null); const resizableRef = useRef<HTMLDivElement>(null);
// chart hook // chart hook
const { currentViewData, scrollLeft } = useChart(); const { currentViewData } = useGanttChart();
// check if cursor reaches either end while resizing/dragging // check if cursor reaches either end while resizing/dragging
const checkScrollEnd = (e: MouseEvent): number => { const checkScrollEnd = (e: MouseEvent): number => {
const SCROLL_THRESHOLD = 70; const SCROLL_THRESHOLD = 70;
@ -212,6 +224,17 @@ export const ChartDraggable: React.FC<Props> = (props) => {
block.position?.width && block.position?.width &&
scrollLeft > block.position.marginLeft + block.position.width; scrollLeft > block.position.marginLeft + block.position.width;
useEffect(() => {
const ganttContainer = ganttContainerRef.current;
if (!ganttContainer) return;
const handleScroll = () => setScrollLeft(ganttContainer.scrollLeft);
ganttContainer.addEventListener("scroll", handleScroll);
return () => {
ganttContainer.removeEventListener("scroll", handleScroll);
};
}, [ganttContainerRef]);
useEffect(() => { useEffect(() => {
const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement; const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement;
const resizableBlock = resizableRef.current; const resizableBlock = resizableRef.current;
@ -234,7 +257,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
return () => { return () => {
observer.unobserve(resizableBlock); observer.unobserve(resizableBlock);
}; };
}, [block.data.name]); }, []);
return ( return (
<> <>
@ -312,4 +335,4 @@ export const ChartDraggable: React.FC<Props> = (props) => {
</div> </div>
</> </>
); );
}; });

View File

@ -1,3 +1,2 @@
export * from "./add-block"; export * from "./add-block";
export * from "./block-structure";
export * from "./draggable"; export * from "./draggable";

View File

@ -0,0 +1 @@
export * from "./use-gantt-chart";

View File

@ -1,13 +0,0 @@
import { useContext } from "react";
// types
import { ChartContextReducer } from "../types";
// context
import { ChartContext } from "../contexts";
export const useChart = (): ChartContextReducer => {
const context = useContext(ChartContext);
if (!context) throw new Error("useChart must be used within a GanttChart");
return context;
};

View File

@ -0,0 +1,11 @@
import { useContext } from "react";
// mobx store
import { GanttStoreContext } from "components/gantt-chart/contexts";
// types
import { IGanttStore } from "store/issue/issue_gantt_view.store";
export const useGanttChart = (): IGanttStore => {
const context = useContext(GanttStoreContext);
if (context === undefined) throw new Error("useGanttChart must be used within GanttStoreProvider");
return context;
};

View File

@ -3,5 +3,5 @@ export * from "./chart";
export * from "./helpers"; export * from "./helpers";
export * from "./hooks"; export * from "./hooks";
export * from "./root"; export * from "./root";
export * from "./types";
export * from "./sidebar"; export * from "./sidebar";
export * from "./types";

View File

@ -2,7 +2,7 @@ import { FC } from "react";
// components // components
import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
// context // context
import { ChartContextProvider } from "./contexts"; import { GanttStoreProvider } from "components/gantt-chart/contexts";
type GanttChartRootProps = { type GanttChartRootProps = {
border?: boolean; border?: boolean;
@ -42,7 +42,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
} = props; } = props;
return ( return (
<ChartContextProvider> <GanttStoreProvider>
<ChartViewRoot <ChartViewRoot
border={border} border={border}
title={title} title={title}
@ -60,6 +60,6 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
showAllBlocks={showAllBlocks} showAllBlocks={showAllBlocks}
quickAdd={quickAdd} quickAdd={quickAdd}
/> />
</ChartContextProvider> </GanttStoreProvider>
); );
}; };

View File

@ -1,158 +0,0 @@
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
// ui
import { Loader } from "@plane/ui";
// components
import { CycleGanttSidebarBlock } from "components/cycles";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types";
// constants
import { BLOCK_HEIGHT } from "../constants";
type Props = {
title: string;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
};
export const CycleGanttSidebar: React.FC<Props> = (props) => {
const { blockUpdateHandler, blocks, enableReorder } = props;
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
activeBlock: block,
},
});
};
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination } = result;
// return if dropped outside the list
if (!destination) return;
// return if dropped on the same index
if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order;
// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => {
const duration = findTotalDaysInRange(block.start_date, block.target_date);
return (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
id={`sidebar-block-${block.id}`}
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": activeBlock?.id === block.id,
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<CycleGanttSidebarBlock cycleId={block.data.id} />
</div>
{duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""}
</div>
)}
</div>
</div>
</div>
)}
</Draggable>
);
})
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
</div>
)}
</Droppable>
</DragDropContext>
);
};

View File

@ -0,0 +1,72 @@
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { observer } from "mobx-react";
import { MoreVertical } from "lucide-react";
// hooks
import { useGanttChart } from "components/gantt-chart/hooks";
// components
import { CycleGanttSidebarBlock } from "components/cycles";
// helpers
import { cn } from "helpers/common.helper";
import { findTotalDaysInRange } from "helpers/date-time.helper";
// types
import { IGanttBlock } from "components/gantt-chart/types";
// constants
import { BLOCK_HEIGHT } from "components/gantt-chart/constants";
type Props = {
block: IGanttBlock;
enableReorder: boolean;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
};
export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
const { block, enableReorder, provided, snapshot } = props;
// store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart();
const duration = findTotalDaysInRange(block.start_date, block.target_date);
return (
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
id={`sidebar-block-${block.id}`}
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": isBlockActive(block.id),
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<CycleGanttSidebarBlock cycleId={block.data.id} />
</div>
{duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""}
</div>
)}
</div>
</div>
</div>
);
});

View File

@ -0,0 +1 @@
export * from "./sidebar";

View File

@ -0,0 +1,100 @@
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
// ui
import { Loader } from "@plane/ui";
// components
import { CyclesSidebarBlock } from "./block";
// types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types";
type Props = {
title: string;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
};
export const CycleGanttSidebar: React.FC<Props> = (props) => {
const { blockUpdateHandler, blocks, enableReorder } = props;
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination } = result;
// return if dropped outside the list
if (!destination) return;
// return if dropped on the same index
if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order;
// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<CyclesSidebarBlock
block={block}
enableReorder={enableReorder}
provided={provided}
snapshot={snapshot}
/>
)}
</Draggable>
))
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
</div>
)}
</Droppable>
</DragDropContext>
);
};

View File

@ -1,173 +0,0 @@
import { observer } from "mobx-react";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
import { useIssueDetail } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
// components
import { IssueGanttSidebarBlock } from "components/issues";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
import { BLOCK_HEIGHT } from "../constants";
type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
showAllBlocks?: boolean;
};
export const IssueGanttSidebar: React.FC<Props> = observer((props: Props) => {
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props;
const { activeBlock, dispatch } = useChart();
const { peekIssue } = useIssueDetail();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
activeBlock: block,
},
});
};
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination } = result;
// return if dropped outside the list
if (!destination) return;
// return if dropped on the same index
if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order;
// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<>
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => {
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
const duration =
!block.start_date || !block.target_date
? null
: findTotalDaysInRange(block.start_date, block.target_date);
return (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
"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)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": activeBlock?.id === block.id,
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<IssueGanttSidebarBlock issueId={block.data.id} />
</div>
{duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
<span>
{duration} day{duration > 1 ? "s" : ""}
</span>
</div>
)}
</div>
</div>
</div>
)}
</Draggable>
);
})
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
</div>
)}
</Droppable>
</DragDropContext>
</>
);
});

View File

@ -0,0 +1,77 @@
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { observer } from "mobx-react";
import { MoreVertical } from "lucide-react";
// hooks
import { useIssueDetail } from "hooks/store";
import { useGanttChart } from "components/gantt-chart/hooks";
// components
import { IssueGanttSidebarBlock } from "components/issues";
// helpers
import { cn } from "helpers/common.helper";
import { findTotalDaysInRange } from "helpers/date-time.helper";
// types
import { IGanttBlock } from "../../types";
// constants
import { BLOCK_HEIGHT } from "../../constants";
type Props = {
block: IGanttBlock;
enableReorder: boolean;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
};
export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
const { block, enableReorder, provided, snapshot } = props;
// store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart();
const { peekIssue } = useIssueDetail();
const duration = findTotalDaysInRange(block.start_date, block.target_date);
return (
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": isBlockActive(block.id),
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<IssueGanttSidebarBlock issueId={block.data.id} />
</div>
{duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
<span>
{duration} day{duration > 1 ? "s" : ""}
</span>
</div>
)}
</div>
</div>
</div>
);
});

View File

@ -0,0 +1 @@
export * from "./sidebar";

View File

@ -0,0 +1,107 @@
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
// components
import { IssuesSidebarBlock } from "./block";
// ui
import { Loader } from "@plane/ui";
// types
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
showAllBlocks?: boolean;
};
export const IssueGanttSidebar: React.FC<Props> = (props) => {
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props;
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination } = result;
// return if dropped outside the list
if (!destination) return;
// return if dropped on the same index
if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order;
// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => {
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
return (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<IssuesSidebarBlock
block={block}
enableReorder={enableReorder}
provided={provided}
snapshot={snapshot}
/>
)}
</Draggable>
);
})
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
</div>
)}
</Droppable>
</DragDropContext>
);
};

View File

@ -1,158 +0,0 @@
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
// ui
import { Loader } from "@plane/ui";
// components
import { ModuleGanttSidebarBlock } from "components/modules";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
// constants
import { BLOCK_HEIGHT } from "../constants";
type Props = {
title: string;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
};
export const ModuleGanttSidebar: React.FC<Props> = (props) => {
const { blockUpdateHandler, blocks, enableReorder } = props;
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
activeBlock: block,
},
});
};
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination } = result;
// return if dropped outside the list
if (!destination) return;
// return if dropped on the same index
if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order;
// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => {
const duration = findTotalDaysInRange(block.start_date, block.target_date);
return (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
id={`sidebar-block-${block.id}`}
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": activeBlock?.id === block.id,
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<ModuleGanttSidebarBlock moduleId={block.data.id} />
</div>
{duration !== undefined && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""}
</div>
)}
</div>
</div>
</div>
)}
</Draggable>
);
})
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
</div>
)}
</Droppable>
</DragDropContext>
);
};

View File

@ -0,0 +1,72 @@
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { observer } from "mobx-react";
import { MoreVertical } from "lucide-react";
// hooks
import { useGanttChart } from "components/gantt-chart/hooks";
// components
import { ModuleGanttSidebarBlock } from "components/modules";
// helpers
import { cn } from "helpers/common.helper";
import { findTotalDaysInRange } from "helpers/date-time.helper";
// types
import { IGanttBlock } from "components/gantt-chart/types";
// constants
import { BLOCK_HEIGHT } from "components/gantt-chart/constants";
type Props = {
block: IGanttBlock;
enableReorder: boolean;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
};
export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
const { block, enableReorder, provided, snapshot } = props;
// store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart();
const duration = findTotalDaysInRange(block.start_date, block.target_date);
return (
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
id={`sidebar-block-${block.id}`}
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": isBlockActive(block.id),
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<ModuleGanttSidebarBlock moduleId={block.data.id} />
</div>
{duration !== undefined && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""}
</div>
)}
</div>
</div>
</div>
);
});

View File

@ -0,0 +1 @@
export * from "./sidebar";

View File

@ -0,0 +1,100 @@
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
// ui
import { Loader } from "@plane/ui";
// components
import { ModulesSidebarBlock } from "./block";
// types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
type Props = {
title: string;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
};
export const ModuleGanttSidebar: React.FC<Props> = (props) => {
const { blockUpdateHandler, blocks, enableReorder } = props;
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination } = result;
// return if dropped outside the list
if (!destination) return;
// return if dropped on the same index
if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order;
// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<ModulesSidebarBlock
block={block}
enableReorder={enableReorder}
provided={provided}
snapshot={snapshot}
/>
)}
</Draggable>
))
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
</div>
)}
</Droppable>
</DragDropContext>
);
};

View File

@ -1,17 +1,10 @@
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { IssueGanttSidebarBlock } from "components/issues"; import { IssuesSidebarBlock } from "./issues/block";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
// types // types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types";
// constants
import { BLOCK_HEIGHT } from "../constants";
type Props = { type Props = {
title: string; title: string;
@ -23,18 +16,6 @@ type Props = {
export const ProjectViewGanttSidebar: React.FC<Props> = (props) => { export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
const { blockUpdateHandler, blocks, enableReorder } = props; const { blockUpdateHandler, blocks, enableReorder } = props;
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
activeBlock: block,
},
});
};
const handleOrderChange = (result: DropResult) => { const handleOrderChange = (result: DropResult) => {
if (!blocks) return; if (!blocks) return;
@ -89,59 +70,23 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
> >
<> <>
{blocks ? ( {blocks ? (
blocks.map((block, index) => { blocks.map((block, index) => (
const duration = findTotalDaysInRange(block.start_date, block.target_date); <Draggable
key={`sidebar-block-${block.id}`}
return ( draggableId={`sidebar-block-${block.id}`}
<Draggable index={index}
key={`sidebar-block-${block.id}`} isDragDisabled={!enableReorder}
draggableId={`sidebar-block-${block.id}`} >
index={index} {(provided, snapshot) => (
isDragDisabled={!enableReorder} <IssuesSidebarBlock
> block={block}
{(provided, snapshot) => ( enableReorder={enableReorder}
<div provided={provided}
className={`${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`} snapshot={snapshot}
style={{ />
height: `${BLOCK_HEIGHT}px`, )}
}} </Draggable>
onMouseEnter={() => updateActiveBlock(block)} ))
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
id={`sidebar-block-${block.id}`}
className={`group flex h-full w-full items-center gap-2 rounded-l px-2 pr-4 ${
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
}`}
>
{enableReorder && (
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<IssueGanttSidebarBlock issueId={block.data.id} />
</div>
{duration !== undefined && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""}
</div>
)}
</div>
</div>
</div>
)}
</Draggable>
);
})
) : ( ) : (
<Loader className="space-y-3 pr-2"> <Loader className="space-y-3 pr-2">
<Loader.Item height="34px" /> <Loader.Item height="34px" />

View File

@ -1,10 +1,3 @@
// context types
export type allViewsType = {
key: string;
title: string;
data: Object | null;
};
export interface IGanttBlock { export interface IGanttBlock {
data: any; data: any;
id: string; id: string;
@ -29,34 +22,6 @@ export interface IBlockUpdateData {
export type TGanttViews = "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year"; export type TGanttViews = "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year";
export interface ChartContextData {
allViews: allViewsType[];
currentView: TGanttViews;
currentViewData: ChartDataType | undefined;
renderView: any;
activeBlock: IGanttBlock | null;
}
export type ChartContextActionPayload =
| {
type: "CURRENT_VIEW";
payload: TGanttViews;
}
| {
type: "CURRENT_VIEW_DATA" | "RENDER_VIEW";
payload: ChartDataType | undefined;
}
| {
type: "PARTIAL_UPDATE";
payload: Partial<ChartContextData>;
};
export interface ChartContextReducer extends ChartContextData {
scrollLeft: number;
updateScrollLeft: (scrollLeft: number) => void;
dispatch: (action: ChartContextActionPayload) => void;
}
// chart render types // chart render types
export interface WeekMonthDataType { export interface WeekMonthDataType {
key: number; key: number;

View File

@ -5,12 +5,9 @@ import { observer } from "mobx-react-lite";
import { useIssues, useUser } from "hooks/store"; import { useIssues, useUser } from "hooks/store";
// components // components
import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues"; import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues";
import { import { GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "components/gantt-chart";
GanttChartRoot, // helpers
IBlockUpdateData, import { renderIssueBlocksStructure } from "helpers/issue.helper";
renderIssueBlocksStructure,
IssueGanttSidebar,
} from "components/gantt-chart";
// types // types
import { TIssue, TUnGroupedIssues } from "@plane/types"; import { TIssue, TUnGroupedIssues } from "@plane/types";
import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle";

View File

@ -235,7 +235,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
</button> </button>
))} ))}
<CustomMenu verticalEllipsis buttonClassName="z-[1]"> <CustomMenu verticalEllipsis buttonClassName="z-[1]" placement="bottom-end">
{isEditingAllowed && ( {isEditingAllowed && (
<> <>
<CustomMenu.MenuItem onClick={handleEditModule}> <CustomMenu.MenuItem onClick={handleEditModule}>

View File

@ -3,6 +3,7 @@ import { v4 as uuidv4 } from "uuid";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import { TIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "@plane/types"; import { TIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "@plane/types";
import { IGanttBlock } from "components/gantt-chart";
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
@ -132,3 +133,12 @@ export const createIssuePayload: (projectId: string, formData: Partial<TIssue>)
return payload; return payload;
}; };
export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] =>
blocks?.map((block) => ({
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: block.start_date ? new Date(block.start_date) : null,
target_date: block.target_date ? new Date(block.target_date) : null,
}));

View File

@ -0,0 +1,95 @@
import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
// helpers
import { currentViewDataWithView } from "components/gantt-chart/data";
// types
import { ChartDataType, TGanttViews } from "components/gantt-chart";
export interface IGanttStore {
// observables
currentView: TGanttViews;
currentViewData: ChartDataType | undefined;
activeBlockId: string | null;
renderView: any;
// computed functions
isBlockActive: (blockId: string) => boolean;
// actions
updateCurrentView: (view: TGanttViews) => void;
updateCurrentViewData: (data: ChartDataType | undefined) => void;
updateActiveBlockId: (blockId: string | null) => void;
updateRenderView: (data: any[]) => void;
}
export class GanttStore implements IGanttStore {
// observables
currentView: TGanttViews = "month";
currentViewData: ChartDataType | undefined = undefined;
activeBlockId: string | null = null;
renderView: any[] = [];
constructor() {
makeObservable(this, {
// observables
currentView: observable.ref,
currentViewData: observable,
activeBlockId: observable.ref,
renderView: observable,
// actions
updateCurrentView: action.bound,
updateCurrentViewData: action.bound,
updateActiveBlockId: action.bound,
updateRenderView: action.bound,
});
this.initGantt();
}
/**
* @description check if block is active
* @param {string} blockId
*/
isBlockActive = computedFn((blockId: string): boolean => this.activeBlockId === blockId);
/**
* @description update current view
* @param {TGanttViews} view
*/
updateCurrentView = (view: TGanttViews) => {
this.currentView = view;
};
/**
* @description update current view data
* @param {ChartDataType | undefined} data
*/
updateCurrentViewData = (data: ChartDataType | undefined) => {
this.currentViewData = data;
};
/**
* @description update active block
* @param {string | null} block
*/
updateActiveBlockId = (blockId: string | null) => {
this.activeBlockId = blockId;
};
/**
* @description update render view
* @param {any[]} data
*/
updateRenderView = (data: any[]) => {
this.renderView = data;
};
/**
* @description initialize gantt chart with month view
*/
initGantt = () => {
const newCurrentViewData = currentViewDataWithView(this.currentView);
runInAction(() => {
this.currentViewData = newCurrentViewData;
});
};
}

View File

@ -194,7 +194,6 @@ export class ModulesStore implements IModuleStore {
runInAction(() => { runInAction(() => {
set(this.moduleMap, [response?.id], response); set(this.moduleMap, [response?.id], response);
}); });
this.fetchModules(workspaceSlug, projectId);
return response; return response;
}); });