forked from github/plane
[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,
|
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">
|
||||||
|
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";
|
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>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -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;
|
|
||||||
};
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from "./add-block";
|
export * from "./add-block";
|
||||||
export * from "./block-structure";
|
|
||||||
export * from "./draggable";
|
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 "./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";
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 { 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" />
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
||||||
|
@ -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}>
|
||||||
|
@ -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,
|
||||||
|
}));
|
||||||
|
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(() => {
|
runInAction(() => {
|
||||||
set(this.moduleMap, [response?.id], response);
|
set(this.moduleMap, [response?.id], response);
|
||||||
});
|
});
|
||||||
this.fetchModules(workspaceSlug, projectId);
|
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user