mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[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:
parent
46e1d46005
commit
ba6479674c
@ -14,7 +14,7 @@ import {
|
||||
IssueListItemProps,
|
||||
} from "components/dashboard/widgets";
|
||||
// ui
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
import { Loader, getButtonStyling } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
import { getRedirectionFilters } from "helpers/dashboard.helper";
|
||||
@ -63,7 +63,12 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
||||
<>
|
||||
<div className="h-full">
|
||||
{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 ? (
|
||||
<>
|
||||
<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">
|
||||
|
106
web/components/gantt-chart/blocks/block.tsx
Normal file
106
web/components/gantt-chart/blocks/block.tsx
Normal 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>
|
||||
);
|
||||
});
|
@ -1,16 +1,10 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { FC } from "react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
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";
|
||||
// components
|
||||
import { GanttChartBlock } from "./block";
|
||||
// types
|
||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||
// constants
|
||||
import { BLOCK_HEIGHT, HEADER_HEIGHT } from "../constants";
|
||||
import { HEADER_HEIGHT } from "../constants";
|
||||
|
||||
export type GanttChartBlocksProps = {
|
||||
itemsContainerWidth: number;
|
||||
@ -21,10 +15,11 @@ export type GanttChartBlocksProps = {
|
||||
enableBlockRightResize: boolean;
|
||||
enableBlockMove: boolean;
|
||||
enableAddBlock: boolean;
|
||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||
showAllBlocks: boolean;
|
||||
};
|
||||
|
||||
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = observer((props) => {
|
||||
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
||||
const {
|
||||
itemsContainerWidth,
|
||||
blocks,
|
||||
@ -34,52 +29,9 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = observer((props)
|
||||
enableBlockRightResize,
|
||||
enableBlockMove,
|
||||
enableAddBlock,
|
||||
ganttContainerRef,
|
||||
showAllBlocks,
|
||||
} = 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 (
|
||||
<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
|
||||
if (!showAllBlocks && !(block.start_date && block.target_date)) return;
|
||||
|
||||
const isBlockVisibleOnChart = block.start_date && block.target_date;
|
||||
|
||||
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": 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
|
||||
<GanttChartBlock
|
||||
block={block}
|
||||
blockToRender={blockToRender}
|
||||
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableAddBlock={enableAddBlock}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
/>
|
||||
) : (
|
||||
enableAddBlock && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { Expand, Shrink } from "lucide-react";
|
||||
// hooks
|
||||
import { useChart } from "../hooks";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// 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 = {
|
||||
blocks: IGanttBlock[] | null;
|
||||
@ -16,10 +19,10 @@ type Props = {
|
||||
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;
|
||||
// chart hook
|
||||
const { currentView, allViews } = useChart();
|
||||
const { currentView } = useGanttChart();
|
||||
|
||||
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">
|
||||
@ -29,7 +32,7 @@ export const GanttChartHeader: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{allViews?.map((chartView: any) => (
|
||||
{VIEWS_LIST.map((chartView: any) => (
|
||||
<div
|
||||
key={chartView?.key}
|
||||
className={cn("cursor-pointer rounded-sm p-1 px-2 text-xs", {
|
||||
@ -56,4 +59,4 @@ export const GanttChartHeader: React.FC<Props> = (props) => {
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,3 +1,7 @@
|
||||
import { useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
// components
|
||||
import {
|
||||
BiWeekChartView,
|
||||
@ -12,7 +16,6 @@ import {
|
||||
TGanttViews,
|
||||
WeekChartView,
|
||||
YearChartView,
|
||||
useChart,
|
||||
} from "components/gantt-chart";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
@ -36,7 +39,7 @@ type Props = {
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
};
|
||||
|
||||
export const GanttChartMainContent: React.FC<Props> = (props) => {
|
||||
export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
blocks,
|
||||
blockToRender,
|
||||
@ -55,13 +58,15 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
|
||||
updateCurrentViewRenderPayload,
|
||||
quickAdd,
|
||||
} = props;
|
||||
// refs
|
||||
const ganttContainerRef = useRef<HTMLDivElement>(null);
|
||||
// chart hook
|
||||
const { currentView, currentViewData, updateScrollLeft } = useChart();
|
||||
const { currentView, currentViewData } = useGanttChart();
|
||||
// handling scroll functionality
|
||||
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
|
||||
|
||||
updateScrollLeft(scrollLeft);
|
||||
// updateScrollLeft(scrollLeft);
|
||||
|
||||
const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth;
|
||||
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
|
||||
@ -95,6 +100,7 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
|
||||
"mb-8": bottomSpacing,
|
||||
}
|
||||
)}
|
||||
ref={ganttContainerRef}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<GanttChartSidebar
|
||||
@ -105,7 +111,7 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
|
||||
title={title}
|
||||
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 />
|
||||
{currentViewData && (
|
||||
<GanttChartBlocksList
|
||||
@ -117,10 +123,11 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableAddBlock={enableAddBlock}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
showAllBlocks={showAllBlocks}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
// components
|
||||
import { GanttChartHeader, useChart, GanttChartMainContent } from "components/gantt-chart";
|
||||
import { GanttChartHeader, GanttChartMainContent } from "components/gantt-chart";
|
||||
// views
|
||||
import {
|
||||
generateMonthChart,
|
||||
@ -34,7 +37,7 @@ type ChartViewRootProps = {
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
};
|
||||
|
||||
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||
export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
const {
|
||||
border,
|
||||
title,
|
||||
@ -57,7 +60,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||
const [fullScreenMode, setFullScreenMode] = useState(false);
|
||||
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
|
||||
// hooks
|
||||
const { currentView, currentViewData, renderView, dispatch } = useChart();
|
||||
const { currentView, currentViewData, renderView, updateCurrentView, updateCurrentViewData, updateRenderView } =
|
||||
useGanttChart();
|
||||
|
||||
// rendering the block structure
|
||||
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
|
||||
@ -87,36 +91,20 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||
|
||||
// updating the prevData, currentData and nextData
|
||||
if (currentRender.payload.length > 0) {
|
||||
updateCurrentViewData(currentRender.state);
|
||||
|
||||
if (side === "left") {
|
||||
dispatch({
|
||||
type: "PARTIAL_UPDATE",
|
||||
payload: {
|
||||
currentView: selectedCurrentView,
|
||||
currentViewData: currentRender.state,
|
||||
renderView: [...currentRender.payload, ...renderView],
|
||||
},
|
||||
});
|
||||
updateCurrentView(selectedCurrentView);
|
||||
updateRenderView([...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],
|
||||
},
|
||||
});
|
||||
updateCurrentView(view);
|
||||
updateRenderView([...renderView, ...currentRender.payload]);
|
||||
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
|
||||
} else {
|
||||
dispatch({
|
||||
type: "PARTIAL_UPDATE",
|
||||
payload: {
|
||||
currentView: view,
|
||||
currentViewData: currentRender.state,
|
||||
renderView: [...currentRender.payload],
|
||||
},
|
||||
});
|
||||
updateCurrentView(view);
|
||||
updateRenderView(currentRender.payload);
|
||||
setItemsContainerWidth(currentRender.scrollWidth);
|
||||
setTimeout(() => {
|
||||
handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate);
|
||||
@ -206,4 +194,4 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "components/gantt-chart";
|
||||
import { observer } from "mobx-react";
|
||||
// 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
|
||||
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
|
||||
const { currentView, currentViewData, renderView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -50,4 +51,4 @@ export const BiWeekChartView: FC<any> = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "../../hooks";
|
||||
import { observer } from "mobx-react";
|
||||
// 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
|
||||
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
|
||||
const { currentView, currentViewData, renderView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -50,4 +51,4 @@ export const DayChartView: FC<any> = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "components/gantt-chart";
|
||||
import { observer } from "mobx-react";
|
||||
// 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
|
||||
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
|
||||
const { currentView, currentViewData, renderView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -50,4 +51,4 @@ export const HourChartView: FC<any> = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useChart } from "components/gantt-chart";
|
||||
import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
@ -8,9 +9,9 @@ import { IMonthBlock } from "../../views";
|
||||
// constants
|
||||
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "components/gantt-chart/constants";
|
||||
|
||||
export const MonthChartView: FC<any> = () => {
|
||||
export const MonthChartView: FC<any> = observer(() => {
|
||||
// chart hook
|
||||
const { currentViewData, renderView } = useChart();
|
||||
const { currentViewData, renderView } = useGanttChart();
|
||||
const monthBlocks: IMonthBlock[] = renderView;
|
||||
|
||||
return (
|
||||
@ -71,4 +72,4 @@ export const MonthChartView: FC<any> = () => {
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "../../hooks";
|
||||
import { observer } from "mobx-react";
|
||||
// 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
|
||||
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
|
||||
const { currentView, currentViewData, renderView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -46,4 +47,4 @@ export const QuarterChartView: FC<any> = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "../../hooks";
|
||||
import { observer } from "mobx-react";
|
||||
// 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
|
||||
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
|
||||
const { currentView, currentViewData, renderView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -50,4 +51,4 @@ export const WeekChartView: FC<any> = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { FC } from "react";
|
||||
// context
|
||||
import { useChart } from "../../hooks";
|
||||
import { observer } from "mobx-react";
|
||||
// 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
|
||||
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
|
||||
const { currentView, currentViewData, renderView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -46,4 +47,4 @@ export const YearChartView: FC<any> = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,57 +1,19 @@
|
||||
import React, { createContext, useState } from "react";
|
||||
// types
|
||||
import { ChartContextData, ChartContextActionPayload, ChartContextReducer } from "../types";
|
||||
// data
|
||||
import { allViewsWithData, currentViewDataWithView } from "../data";
|
||||
import { createContext } from "react";
|
||||
// mobx store
|
||||
import { GanttStore } from "store/issue/issue_gantt_view.store";
|
||||
|
||||
export const ChartContext = createContext<ChartContextReducer | undefined>(undefined);
|
||||
let ganttViewStore = new GanttStore();
|
||||
|
||||
const chartReducer = (state: ChartContextData, action: ChartContextActionPayload): ChartContextData => {
|
||||
switch (action.type) {
|
||||
case "CURRENT_VIEW":
|
||||
return { ...state, currentView: action.payload };
|
||||
case "CURRENT_VIEW_DATA":
|
||||
return { ...state, currentViewData: action.payload };
|
||||
case "RENDER_VIEW":
|
||||
return { ...state, currentViewData: action.payload };
|
||||
case "PARTIAL_UPDATE":
|
||||
return { ...state, ...action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
export const GanttStoreContext = createContext<GanttStore>(ganttViewStore);
|
||||
|
||||
const initializeStore = () => {
|
||||
const _ganttStore = ganttViewStore ?? new GanttStore();
|
||||
if (typeof window === "undefined") return _ganttStore;
|
||||
if (!ganttViewStore) ganttViewStore = _ganttStore;
|
||||
return _ganttStore;
|
||||
};
|
||||
|
||||
const initialView = "month";
|
||||
|
||||
export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
// 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>
|
||||
);
|
||||
export const GanttStoreProvider = ({ children }: any) => {
|
||||
const store = initializeStore();
|
||||
return <GanttStoreContext.Provider value={store}>{children}</GanttStoreContext.Provider>;
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
// types
|
||||
import { WeekMonthDataType, ChartDataType } from "../types";
|
||||
import { WeekMonthDataType, ChartDataType, TGanttViews } from "../types";
|
||||
|
||||
// constants
|
||||
export const weeks: WeekMonthDataType[] = [
|
||||
@ -53,7 +53,7 @@ export const datePreview = (date: Date, includeTime: boolean = false) => {
|
||||
};
|
||||
|
||||
// context data
|
||||
export const allViewsWithData: ChartDataType[] = [
|
||||
export const VIEWS_LIST: ChartDataType[] = [
|
||||
// {
|
||||
// key: "hours",
|
||||
// title: "Hours",
|
||||
@ -133,7 +133,5 @@ export const allViewsWithData: ChartDataType[] = [
|
||||
// },
|
||||
];
|
||||
|
||||
export const currentViewDataWithView = (view: string = "month") => {
|
||||
const currentView: ChartDataType | undefined = allViewsWithData.find((_viewData) => _viewData.key === view);
|
||||
return currentView;
|
||||
};
|
||||
export const currentViewDataWithView = (view: TGanttViews = "month") =>
|
||||
VIEWS_LIST.find((_viewData) => _viewData.key === view);
|
||||
|
@ -1,21 +1,21 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { addDays } from "date-fns";
|
||||
import { Plus } from "lucide-react";
|
||||
// hooks
|
||||
import { useChart } from "../hooks";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
type Props = {
|
||||
block: IGanttBlock;
|
||||
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;
|
||||
// states
|
||||
const [isButtonVisible, setIsButtonVisible] = useState(false);
|
||||
@ -24,7 +24,7 @@ export const ChartAddBlock: React.FC<Props> = (props) => {
|
||||
// refs
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// chart hook
|
||||
const { currentViewData } = useChart();
|
||||
const { currentViewData } = useGanttChart();
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (!currentViewData) return;
|
||||
@ -88,4 +88,4 @@ export const ChartAddBlock: React.FC<Props> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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,
|
||||
}));
|
@ -1,11 +1,13 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
// hooks
|
||||
import { IGanttBlock, useChart } from "components/gantt-chart";
|
||||
import { IGanttBlock } from "components/gantt-chart";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// constants
|
||||
import { SIDEBAR_WIDTH } from "../constants";
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
type Props = {
|
||||
block: IGanttBlock;
|
||||
@ -14,19 +16,29 @@ type Props = {
|
||||
enableBlockLeftResize: boolean;
|
||||
enableBlockRightResize: boolean;
|
||||
enableBlockMove: boolean;
|
||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const ChartDraggable: React.FC<Props> = (props) => {
|
||||
const { block, blockToRender, handleBlock, enableBlockLeftResize, enableBlockRightResize, enableBlockMove } = props;
|
||||
export const ChartDraggable: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
block,
|
||||
blockToRender,
|
||||
handleBlock,
|
||||
enableBlockLeftResize,
|
||||
enableBlockRightResize,
|
||||
enableBlockMove,
|
||||
ganttContainerRef,
|
||||
} = props;
|
||||
// states
|
||||
const [isLeftResizing, setIsLeftResizing] = useState(false);
|
||||
const [isRightResizing, setIsRightResizing] = useState(false);
|
||||
const [isMoving, setIsMoving] = useState(false);
|
||||
const [isHidden, setIsHidden] = useState(true);
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
// refs
|
||||
const resizableRef = useRef<HTMLDivElement>(null);
|
||||
// chart hook
|
||||
const { currentViewData, scrollLeft } = useChart();
|
||||
const { currentViewData } = useGanttChart();
|
||||
// check if cursor reaches either end while resizing/dragging
|
||||
const checkScrollEnd = (e: MouseEvent): number => {
|
||||
const SCROLL_THRESHOLD = 70;
|
||||
@ -212,6 +224,17 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
||||
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(() => {
|
||||
const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||
const resizableBlock = resizableRef.current;
|
||||
@ -234,7 +257,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
||||
return () => {
|
||||
observer.unobserve(resizableBlock);
|
||||
};
|
||||
}, [block.data.name]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -312,4 +335,4 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from "./add-block";
|
||||
export * from "./block-structure";
|
||||
export * from "./draggable";
|
||||
|
1
web/components/gantt-chart/hooks/index.ts
Normal file
1
web/components/gantt-chart/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./use-gantt-chart";
|
@ -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;
|
||||
};
|
11
web/components/gantt-chart/hooks/use-gantt-chart.ts
Normal file
11
web/components/gantt-chart/hooks/use-gantt-chart.ts
Normal 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;
|
||||
};
|
@ -3,5 +3,5 @@ export * from "./chart";
|
||||
export * from "./helpers";
|
||||
export * from "./hooks";
|
||||
export * from "./root";
|
||||
export * from "./types";
|
||||
export * from "./sidebar";
|
||||
export * from "./types";
|
||||
|
@ -2,7 +2,7 @@ import { FC } from "react";
|
||||
// components
|
||||
import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
|
||||
// context
|
||||
import { ChartContextProvider } from "./contexts";
|
||||
import { GanttStoreProvider } from "components/gantt-chart/contexts";
|
||||
|
||||
type GanttChartRootProps = {
|
||||
border?: boolean;
|
||||
@ -42,7 +42,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ChartContextProvider>
|
||||
<GanttStoreProvider>
|
||||
<ChartViewRoot
|
||||
border={border}
|
||||
title={title}
|
||||
@ -60,6 +60,6 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||
showAllBlocks={showAllBlocks}
|
||||
quickAdd={quickAdd}
|
||||
/>
|
||||
</ChartContextProvider>
|
||||
</GanttStoreProvider>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
72
web/components/gantt-chart/sidebar/cycles/block.tsx
Normal file
72
web/components/gantt-chart/sidebar/cycles/block.tsx
Normal 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>
|
||||
);
|
||||
});
|
1
web/components/gantt-chart/sidebar/cycles/index.ts
Normal file
1
web/components/gantt-chart/sidebar/cycles/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./sidebar";
|
100
web/components/gantt-chart/sidebar/cycles/sidebar.tsx
Normal file
100
web/components/gantt-chart/sidebar/cycles/sidebar.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
77
web/components/gantt-chart/sidebar/issues/block.tsx
Normal file
77
web/components/gantt-chart/sidebar/issues/block.tsx
Normal 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>
|
||||
);
|
||||
});
|
1
web/components/gantt-chart/sidebar/issues/index.ts
Normal file
1
web/components/gantt-chart/sidebar/issues/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./sidebar";
|
107
web/components/gantt-chart/sidebar/issues/sidebar.tsx
Normal file
107
web/components/gantt-chart/sidebar/issues/sidebar.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
72
web/components/gantt-chart/sidebar/modules/block.tsx
Normal file
72
web/components/gantt-chart/sidebar/modules/block.tsx
Normal 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>
|
||||
);
|
||||
});
|
1
web/components/gantt-chart/sidebar/modules/index.ts
Normal file
1
web/components/gantt-chart/sidebar/modules/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./sidebar";
|
100
web/components/gantt-chart/sidebar/modules/sidebar.tsx
Normal file
100
web/components/gantt-chart/sidebar/modules/sidebar.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1,17 +1,10 @@
|
||||
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 { IssueGanttSidebarBlock } from "components/issues";
|
||||
// helpers
|
||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||
import { IssuesSidebarBlock } from "./issues/block";
|
||||
// types
|
||||
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types";
|
||||
// constants
|
||||
import { BLOCK_HEIGHT } from "../constants";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
@ -23,18 +16,6 @@ type Props = {
|
||||
|
||||
export const ProjectViewGanttSidebar: 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;
|
||||
@ -89,10 +70,7 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
||||
>
|
||||
<>
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => {
|
||||
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
||||
|
||||
return (
|
||||
blocks.map((block, index) => (
|
||||
<Draggable
|
||||
key={`sidebar-block-${block.id}`}
|
||||
draggableId={`sidebar-block-${block.id}`}
|
||||
@ -100,48 +78,15 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
||||
isDragDisabled={!enableReorder}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
|
||||
style={{
|
||||
height: `${BLOCK_HEIGHT}px`,
|
||||
}}
|
||||
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>
|
||||
<IssuesSidebarBlock
|
||||
block={block}
|
||||
enableReorder={enableReorder}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})
|
||||
))
|
||||
) : (
|
||||
<Loader className="space-y-3 pr-2">
|
||||
<Loader.Item height="34px" />
|
||||
|
@ -1,10 +1,3 @@
|
||||
// context types
|
||||
export type allViewsType = {
|
||||
key: string;
|
||||
title: string;
|
||||
data: Object | null;
|
||||
};
|
||||
|
||||
export interface IGanttBlock {
|
||||
data: any;
|
||||
id: string;
|
||||
@ -29,34 +22,6 @@ export interface IBlockUpdateData {
|
||||
|
||||
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
|
||||
export interface WeekMonthDataType {
|
||||
key: number;
|
||||
|
@ -5,12 +5,9 @@ import { observer } from "mobx-react-lite";
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
// components
|
||||
import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues";
|
||||
import {
|
||||
GanttChartRoot,
|
||||
IBlockUpdateData,
|
||||
renderIssueBlocksStructure,
|
||||
IssueGanttSidebar,
|
||||
} from "components/gantt-chart";
|
||||
import { GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "components/gantt-chart";
|
||||
// helpers
|
||||
import { renderIssueBlocksStructure } from "helpers/issue.helper";
|
||||
// types
|
||||
import { TIssue, TUnGroupedIssues } from "@plane/types";
|
||||
import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle";
|
||||
|
@ -235,7 +235,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
))}
|
||||
|
||||
<CustomMenu verticalEllipsis buttonClassName="z-[1]">
|
||||
<CustomMenu verticalEllipsis buttonClassName="z-[1]" placement="bottom-end">
|
||||
{isEditingAllowed && (
|
||||
<>
|
||||
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||
|
@ -3,6 +3,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { TIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "@plane/types";
|
||||
import { IGanttBlock } from "components/gantt-chart";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
|
||||
@ -132,3 +133,12 @@ export const createIssuePayload: (projectId: string, formData: Partial<TIssue>)
|
||||
|
||||
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,
|
||||
}));
|
||||
|
95
web/store/issue/issue_gantt_view.store.ts
Normal file
95
web/store/issue/issue_gantt_view.store.ts
Normal 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;
|
||||
});
|
||||
};
|
||||
}
|
@ -194,7 +194,6 @@ export class ModulesStore implements IModuleStore {
|
||||
runInAction(() => {
|
||||
set(this.moduleMap, [response?.id], response);
|
||||
});
|
||||
this.fetchModules(workspaceSlug, projectId);
|
||||
return response;
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user