forked from github/plane
refactor: Gantt chart layout (#3585)
* chore: gantt sidebar and main content scroll sync * chore: add arrow navigation position logic * refactor: scroll position update logic * refactor: gantt chart components * refactor: gantt sidebar * fix: vertical scroll issue * fix: move to the hidden block button flickering * refactor: gantt sidebar components * chore: move timeline header outside * fix gantt scroll issue * fix: sticky position issues * fix: infinite timeline scroll logic * chore: removed unnecessary import statements --------- Co-authored-by: rahulramesha <rahulramesham@gmail.com>
This commit is contained in:
parent
3eb819c4ae
commit
963d26ccda
@ -1,16 +1,30 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// hooks
|
||||||
|
import { useApplication, useCycle } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, ContrastIcon } from "@plane/ui";
|
import { Tooltip, ContrastIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
// types
|
|
||||||
import { ICycle } from "@plane/types";
|
|
||||||
|
|
||||||
export const CycleGanttBlock = ({ data }: { data: ICycle }) => {
|
type Props = {
|
||||||
|
cycleId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CycleGanttBlock: React.FC<Props> = observer((props) => {
|
||||||
|
const { cycleId } = props;
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
// store hooks
|
||||||
|
const {
|
||||||
|
router: { workspaceSlug },
|
||||||
|
} = useApplication();
|
||||||
|
const { getCycleById } = useCycle();
|
||||||
|
// derived values
|
||||||
|
const cycleDetails = getCycleById(cycleId);
|
||||||
|
|
||||||
|
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
|
||||||
|
|
||||||
const cycleStatus = data.status.toLocaleLowerCase();
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full items-center rounded"
|
className="relative flex h-full w-full items-center rounded"
|
||||||
@ -26,36 +40,45 @@ export const CycleGanttBlock = ({ data }: { data: ICycle }) => {
|
|||||||
? "rgb(var(--color-text-200))"
|
? "rgb(var(--color-text-200))"
|
||||||
: "",
|
: "",
|
||||||
}}
|
}}
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)}
|
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)}
|
||||||
>
|
>
|
||||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h5>{data?.name}</h5>
|
<h5>{cycleDetails?.name}</h5>
|
||||||
<div>
|
<div>
|
||||||
{renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.end_date ?? "")}
|
{renderFormattedDate(cycleDetails?.start_date ?? "")} to{" "}
|
||||||
|
{renderFormattedDate(cycleDetails?.end_date ?? "")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
position="top-left"
|
position="top-left"
|
||||||
>
|
>
|
||||||
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{data?.name}</div>
|
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{cycleDetails?.name}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => {
|
export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
||||||
|
const { cycleId } = props;
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
// store hooks
|
||||||
|
const {
|
||||||
|
router: { workspaceSlug },
|
||||||
|
} = useApplication();
|
||||||
|
const { getCycleById } = useCycle();
|
||||||
|
// derived values
|
||||||
|
const cycleDetails = getCycleById(cycleId);
|
||||||
|
|
||||||
const cycleStatus = data.status.toLocaleLowerCase();
|
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full items-center gap-2"
|
className="relative flex h-full w-full items-center gap-2"
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)}
|
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)}
|
||||||
>
|
>
|
||||||
<ContrastIcon
|
<ContrastIcon
|
||||||
className="h-5 w-5 flex-shrink-0"
|
className="h-5 w-5 flex-shrink-0"
|
||||||
@ -71,7 +94,7 @@ export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => {
|
|||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<h6 className="flex-grow truncate text-sm font-medium">{data?.name}</h6>
|
<h6 className="flex-grow truncate text-sm font-medium">{cycleDetails?.name}</h6>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -63,7 +63,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
|||||||
blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null}
|
blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null}
|
||||||
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
|
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
|
||||||
sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
|
sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
|
||||||
blockToRender={(data: ICycle) => <CycleGanttBlock data={data} />}
|
blockToRender={(data: ICycle) => <CycleGanttBlock cycleId={data.id} />}
|
||||||
enableBlockLeftResize={false}
|
enableBlockLeftResize={false}
|
||||||
enableBlockRightResize={false}
|
enableBlockRightResize={false}
|
||||||
enableBlockMove={false}
|
enableBlockMove={false}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
@ -8,6 +9,8 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||||
|
// constants
|
||||||
|
import { BLOCK_HEIGHT, HEADER_HEIGHT } from "../constants";
|
||||||
|
|
||||||
export type GanttChartBlocksProps = {
|
export type GanttChartBlocksProps = {
|
||||||
itemsContainerWidth: number;
|
itemsContainerWidth: number;
|
||||||
@ -20,7 +23,7 @@ export type GanttChartBlocksProps = {
|
|||||||
showAllBlocks: boolean;
|
showAllBlocks: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
|
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
itemsContainerWidth,
|
itemsContainerWidth,
|
||||||
blocks,
|
blocks,
|
||||||
@ -31,9 +34,10 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
|
|||||||
enableBlockMove,
|
enableBlockMove,
|
||||||
showAllBlocks,
|
showAllBlocks,
|
||||||
} = props;
|
} = props;
|
||||||
|
// store hooks
|
||||||
const { activeBlock, dispatch } = useChart();
|
|
||||||
const { peekIssue } = useIssueDetail();
|
const { peekIssue } = useIssueDetail();
|
||||||
|
// chart hook
|
||||||
|
const { activeBlock, dispatch } = useChart();
|
||||||
|
|
||||||
// update the active block on hover
|
// update the active block on hover
|
||||||
const updateActiveBlock = (block: IGanttBlock | null) => {
|
const updateActiveBlock = (block: IGanttBlock | null) => {
|
||||||
@ -77,43 +81,51 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto"
|
className="h-full"
|
||||||
style={{ width: `${itemsContainerWidth}px` }}
|
style={{
|
||||||
|
width: `${itemsContainerWidth}px`,
|
||||||
|
marginTop: `${HEADER_HEIGHT}px`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{blocks &&
|
{blocks?.map((block) => {
|
||||||
blocks.length > 0 &&
|
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||||
blocks.map((block) => {
|
if (!showAllBlocks && !(block.start_date && block.target_date)) return;
|
||||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
|
||||||
if (!showAllBlocks && !(block.start_date && block.target_date)) return;
|
|
||||||
|
|
||||||
const isBlockVisibleOnChart = block.start_date && block.target_date;
|
const isBlockVisibleOnChart = block.start_date && block.target_date;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
key={`block-${block.id}`}
|
||||||
|
className="relative min-w-full w-max"
|
||||||
|
style={{
|
||||||
|
height: `${BLOCK_HEIGHT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
key={`block-${block.id}`}
|
className={cn("relative h-full", {
|
||||||
className={cn(
|
"bg-custom-background-80": activeBlock?.id === block.id,
|
||||||
"h-11",
|
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
|
||||||
{ "rounded bg-custom-background-80": activeBlock?.id === block.id },
|
peekIssue?.issueId === block.data.id,
|
||||||
{
|
})}
|
||||||
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
|
|
||||||
peekIssue?.issueId === block.data.id,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onMouseEnter={() => updateActiveBlock(block)}
|
onMouseEnter={() => updateActiveBlock(block)}
|
||||||
onMouseLeave={() => updateActiveBlock(null)}
|
onMouseLeave={() => updateActiveBlock(null)}
|
||||||
>
|
>
|
||||||
{!isBlockVisibleOnChart && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />}
|
{isBlockVisibleOnChart ? (
|
||||||
<ChartDraggable
|
<ChartDraggable
|
||||||
block={block}
|
block={block}
|
||||||
blockToRender={blockToRender}
|
blockToRender={blockToRender}
|
||||||
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
|
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
|
||||||
enableBlockLeftResize={enableBlockLeftResize}
|
enableBlockLeftResize={enableBlockLeftResize}
|
||||||
enableBlockRightResize={enableBlockRightResize}
|
enableBlockRightResize={enableBlockRightResize}
|
||||||
enableBlockMove={enableBlockMove}
|
enableBlockMove={enableBlockMove}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
@ -1 +1 @@
|
|||||||
export * from "./blocks-display";
|
export * from "./blocks-list";
|
||||||
|
59
web/components/gantt-chart/chart/header.tsx
Normal file
59
web/components/gantt-chart/chart/header.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { Expand, Shrink } from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import { useChart } from "../hooks";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
// types
|
||||||
|
import { IGanttBlock, TGanttViews } from "../types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
blocks: IGanttBlock[] | null;
|
||||||
|
fullScreenMode: boolean;
|
||||||
|
handleChartView: (view: TGanttViews) => void;
|
||||||
|
handleToday: () => void;
|
||||||
|
loaderTitle: string;
|
||||||
|
title: string;
|
||||||
|
toggleFullScreenMode: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GanttChartHeader: React.FC<Props> = (props) => {
|
||||||
|
const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props;
|
||||||
|
// chart hook
|
||||||
|
const { currentView, allViews } = useChart();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2 z-10">
|
||||||
|
<div className="flex items-center gap-2 text-lg font-medium">{title}</div>
|
||||||
|
<div className="ml-auto">
|
||||||
|
<div className="ml-auto text-sm font-medium">{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{allViews?.map((chartView: any) => (
|
||||||
|
<div
|
||||||
|
key={chartView?.key}
|
||||||
|
className={cn("cursor-pointer rounded-sm p-1 px-2 text-xs", {
|
||||||
|
"bg-custom-background-80": currentView === chartView?.key,
|
||||||
|
"hover:bg-custom-background-90": currentView !== chartView?.key,
|
||||||
|
})}
|
||||||
|
onClick={() => handleChartView(chartView?.key)}
|
||||||
|
>
|
||||||
|
{chartView?.title}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" className="rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80" onClick={handleToday}>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center justify-center rounded-sm border border-custom-border-200 p-1 transition-all hover:bg-custom-background-80"
|
||||||
|
onClick={toggleFullScreenMode}
|
||||||
|
>
|
||||||
|
{fullScreenMode ? <Shrink className="h-4 w-4" /> : <Expand className="h-4 w-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
4
web/components/gantt-chart/chart/index.ts
Normal file
4
web/components/gantt-chart/chart/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./views";
|
||||||
|
export * from "./header";
|
||||||
|
export * from "./main-content";
|
||||||
|
export * from "./root";
|
@ -1,324 +0,0 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
|
||||||
// icons
|
|
||||||
// components
|
|
||||||
import { GanttChartBlocks } from "components/gantt-chart";
|
|
||||||
// import { GanttSidebar } from "../sidebar";
|
|
||||||
// import { HourChartView } from "./hours";
|
|
||||||
// import { DayChartView } from "./day";
|
|
||||||
// import { WeekChartView } from "./week";
|
|
||||||
// import { BiWeekChartView } from "./bi-week";
|
|
||||||
import { MonthChartView } from "./month";
|
|
||||||
// import { QuarterChartView } from "./quarter";
|
|
||||||
// import { YearChartView } from "./year";
|
|
||||||
// icons
|
|
||||||
import { Expand, Shrink } from "lucide-react";
|
|
||||||
// views
|
|
||||||
import {
|
|
||||||
// generateHourChart,
|
|
||||||
// generateDayChart,
|
|
||||||
// generateWeekChart,
|
|
||||||
// generateBiWeekChart,
|
|
||||||
generateMonthChart,
|
|
||||||
// generateQuarterChart,
|
|
||||||
// generateYearChart,
|
|
||||||
getNumberOfDaysBetweenTwoDatesInMonth,
|
|
||||||
// getNumberOfDaysBetweenTwoDatesInQuarter,
|
|
||||||
// getNumberOfDaysBetweenTwoDatesInYear,
|
|
||||||
getMonthChartItemPositionWidthInMonth,
|
|
||||||
} from "../views";
|
|
||||||
// types
|
|
||||||
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
|
|
||||||
// data
|
|
||||||
import { currentViewDataWithView } from "../data";
|
|
||||||
// context
|
|
||||||
import { useChart } from "../hooks";
|
|
||||||
|
|
||||||
type ChartViewRootProps = {
|
|
||||||
border: boolean;
|
|
||||||
title: string;
|
|
||||||
loaderTitle: string;
|
|
||||||
blocks: IGanttBlock[] | null;
|
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
|
||||||
blockToRender: (data: any) => React.ReactNode;
|
|
||||||
sidebarToRender: (props: any) => React.ReactNode;
|
|
||||||
enableBlockLeftResize: boolean;
|
|
||||||
enableBlockRightResize: boolean;
|
|
||||||
enableBlockMove: boolean;
|
|
||||||
enableReorder: boolean;
|
|
||||||
bottomSpacing: boolean;
|
|
||||||
showAllBlocks: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
|
||||||
const {
|
|
||||||
border,
|
|
||||||
title,
|
|
||||||
blocks = null,
|
|
||||||
loaderTitle,
|
|
||||||
blockUpdateHandler,
|
|
||||||
sidebarToRender,
|
|
||||||
blockToRender,
|
|
||||||
enableBlockLeftResize,
|
|
||||||
enableBlockRightResize,
|
|
||||||
enableBlockMove,
|
|
||||||
enableReorder,
|
|
||||||
bottomSpacing,
|
|
||||||
showAllBlocks,
|
|
||||||
} = props;
|
|
||||||
// states
|
|
||||||
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
|
|
||||||
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
|
|
||||||
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null); // blocks state management starts
|
|
||||||
// hooks
|
|
||||||
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = useChart();
|
|
||||||
|
|
||||||
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
|
|
||||||
blocks && blocks.length > 0
|
|
||||||
? blocks.map((block: any) => ({
|
|
||||||
...block,
|
|
||||||
position: getMonthChartItemPositionWidthInMonth(view, block),
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentViewData && blocks) setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
|
|
||||||
}, [currentViewData, blocks]);
|
|
||||||
|
|
||||||
// blocks state management ends
|
|
||||||
|
|
||||||
const handleChartView = (key: TGanttViews) => updateCurrentViewRenderPayload(null, key);
|
|
||||||
|
|
||||||
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
|
|
||||||
const selectedCurrentView: TGanttViews = view;
|
|
||||||
const selectedCurrentViewData: ChartDataType | undefined =
|
|
||||||
selectedCurrentView && selectedCurrentView === currentViewData?.key
|
|
||||||
? currentViewData
|
|
||||||
: currentViewDataWithView(view);
|
|
||||||
|
|
||||||
if (selectedCurrentViewData === undefined) return;
|
|
||||||
|
|
||||||
let currentRender: any;
|
|
||||||
|
|
||||||
// if (view === "hours") currentRender = generateHourChart(selectedCurrentViewData, side);
|
|
||||||
// if (view === "day") currentRender = generateDayChart(selectedCurrentViewData, side);
|
|
||||||
// if (view === "week") currentRender = generateWeekChart(selectedCurrentViewData, side);
|
|
||||||
// if (view === "bi_week") currentRender = generateBiWeekChart(selectedCurrentViewData, side);
|
|
||||||
if (selectedCurrentView === "month") currentRender = generateMonthChart(selectedCurrentViewData, side);
|
|
||||||
// if (view === "quarter") currentRender = generateQuarterChart(selectedCurrentViewData, side);
|
|
||||||
// if (selectedCurrentView === "year")
|
|
||||||
// currentRender = generateYearChart(selectedCurrentViewData, side);
|
|
||||||
|
|
||||||
// updating the prevData, currentData and nextData
|
|
||||||
if (currentRender.payload.length > 0) {
|
|
||||||
if (side === "left") {
|
|
||||||
dispatch({
|
|
||||||
type: "PARTIAL_UPDATE",
|
|
||||||
payload: {
|
|
||||||
currentView: selectedCurrentView,
|
|
||||||
currentViewData: currentRender.state,
|
|
||||||
renderView: [...currentRender.payload, ...renderView],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
|
|
||||||
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
|
|
||||||
} else if (side === "right") {
|
|
||||||
dispatch({
|
|
||||||
type: "PARTIAL_UPDATE",
|
|
||||||
payload: {
|
|
||||||
currentView: view,
|
|
||||||
currentViewData: currentRender.state,
|
|
||||||
renderView: [...renderView, ...currentRender.payload],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
|
|
||||||
} else {
|
|
||||||
dispatch({
|
|
||||||
type: "PARTIAL_UPDATE",
|
|
||||||
payload: {
|
|
||||||
currentView: view,
|
|
||||||
currentViewData: currentRender.state,
|
|
||||||
renderView: [...currentRender.payload],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setItemsContainerWidth(currentRender.scrollWidth);
|
|
||||||
setTimeout(() => {
|
|
||||||
handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate);
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToday = () => updateCurrentViewRenderPayload(null, currentView);
|
|
||||||
|
|
||||||
// handling the scroll positioning from left and right
|
|
||||||
useEffect(() => {
|
|
||||||
handleToday();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updatingCurrentLeftScrollPosition = (width: number) => {
|
|
||||||
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
|
||||||
|
|
||||||
if (!scrollContainer) return;
|
|
||||||
|
|
||||||
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
|
|
||||||
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
|
|
||||||
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
|
||||||
|
|
||||||
if (!scrollContainer) return;
|
|
||||||
|
|
||||||
const clientVisibleWidth: number = scrollContainer?.clientWidth;
|
|
||||||
let scrollWidth: number = 0;
|
|
||||||
let daysDifference: number = 0;
|
|
||||||
|
|
||||||
// if (currentView === "hours")
|
|
||||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
|
||||||
// if (currentView === "day")
|
|
||||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
|
||||||
// if (currentView === "week")
|
|
||||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
|
||||||
// if (currentView === "bi_week")
|
|
||||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
|
||||||
if (currentView === "month")
|
|
||||||
daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
|
||||||
// if (currentView === "quarter")
|
|
||||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date);
|
|
||||||
// if (currentView === "year")
|
|
||||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date);
|
|
||||||
|
|
||||||
scrollWidth = daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width);
|
|
||||||
|
|
||||||
scrollContainer.scrollLeft = scrollWidth;
|
|
||||||
};
|
|
||||||
|
|
||||||
// handling scroll functionality
|
|
||||||
const onScroll = () => {
|
|
||||||
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
|
||||||
|
|
||||||
if (!scrollContainer) return;
|
|
||||||
|
|
||||||
const scrollWidth: number = scrollContainer?.scrollWidth;
|
|
||||||
const clientVisibleWidth: number = scrollContainer?.clientWidth;
|
|
||||||
const currentScrollPosition: number = scrollContainer?.scrollLeft;
|
|
||||||
|
|
||||||
updateScrollLeft(currentScrollPosition);
|
|
||||||
|
|
||||||
const approxRangeLeft: number = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
|
|
||||||
const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth);
|
|
||||||
|
|
||||||
if (currentScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView);
|
|
||||||
if (currentScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
fullScreenMode ? `fixed bottom-0 left-0 right-0 top-0 z-[999999] bg-custom-background-100` : `relative`
|
|
||||||
} ${
|
|
||||||
border ? `border border-custom-border-200` : ``
|
|
||||||
} flex h-full select-none flex-col rounded-sm bg-custom-background-100 shadow`}
|
|
||||||
>
|
|
||||||
{/* chart header */}
|
|
||||||
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2">
|
|
||||||
{title && (
|
|
||||||
<div className="flex items-center gap-2 text-lg font-medium">
|
|
||||||
<div>{title}</div>
|
|
||||||
{/* <div className="text-xs rounded-full px-2 py-1 font-bold border border-custom-primary/75 bg-custom-primary/5 text-custom-text-100">
|
|
||||||
Gantt View Beta
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="ml-auto">
|
|
||||||
{blocks === null ? (
|
|
||||||
<div className="ml-auto text-sm font-medium">Loading...</div>
|
|
||||||
) : (
|
|
||||||
<div className="ml-auto text-sm font-medium">
|
|
||||||
{blocks.length} {loaderTitle}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
{allViews &&
|
|
||||||
allViews.length > 0 &&
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
allViews.map((_chatView: any, _idx: any) => (
|
|
||||||
<div
|
|
||||||
key={_chatView?.key}
|
|
||||||
className={`cursor-pointer rounded-sm p-1 px-2 text-xs ${
|
|
||||||
currentView === _chatView?.key ? `bg-custom-background-80` : `hover:bg-custom-background-90`
|
|
||||||
}`}
|
|
||||||
onClick={() => handleChartView(_chatView?.key)}
|
|
||||||
>
|
|
||||||
{_chatView?.title}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div
|
|
||||||
className="cursor-pointer rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80"
|
|
||||||
onClick={handleToday}
|
|
||||||
>
|
|
||||||
Today
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="flex cursor-pointer items-center justify-center rounded-sm border border-custom-border-200 p-1 transition-all hover:bg-custom-background-80"
|
|
||||||
onClick={() => setFullScreenMode((prevData) => !prevData)}
|
|
||||||
>
|
|
||||||
{fullScreenMode ? <Shrink className="h-4 w-4" /> : <Expand className="h-4 w-4" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* content */}
|
|
||||||
<div
|
|
||||||
id="gantt-container"
|
|
||||||
className={`relative flex h-full w-full flex-1 overflow-hidden border-t border-custom-border-200 ${
|
|
||||||
bottomSpacing ? "mb-8" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div id="gantt-sidebar" className="flex h-full w-1/4 flex-col border-r border-custom-border-200">
|
|
||||||
<div className="box-border flex h-[60px] flex-shrink-0 items-end justify-between gap-2 border-b border-custom-border-200 pb-2 pl-10 pr-4 text-sm font-medium text-custom-text-300">
|
|
||||||
<h6>{title}</h6>
|
|
||||||
<h6>Duration</h6>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="horizontal-scroll-enable relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto"
|
|
||||||
id="scroll-container"
|
|
||||||
onScroll={onScroll}
|
|
||||||
>
|
|
||||||
{/* {currentView && currentView === "hours" && <HourChartView />} */}
|
|
||||||
{/* {currentView && currentView === "day" && <DayChartView />} */}
|
|
||||||
{/* {currentView && currentView === "week" && <WeekChartView />} */}
|
|
||||||
{/* {currentView && currentView === "bi_week" && <BiWeekChartView />} */}
|
|
||||||
{currentView && currentView === "month" && <MonthChartView />}
|
|
||||||
{/* {currentView && currentView === "quarter" && <QuarterChartView />} */}
|
|
||||||
{/* {currentView && currentView === "year" && <YearChartView />} */}
|
|
||||||
|
|
||||||
{/* blocks */}
|
|
||||||
{currentView && currentViewData && (
|
|
||||||
<GanttChartBlocks
|
|
||||||
itemsContainerWidth={itemsContainerWidth}
|
|
||||||
blocks={chartBlocks}
|
|
||||||
blockToRender={blockToRender}
|
|
||||||
blockUpdateHandler={blockUpdateHandler}
|
|
||||||
enableBlockLeftResize={enableBlockLeftResize}
|
|
||||||
enableBlockRightResize={enableBlockRightResize}
|
|
||||||
enableBlockMove={enableBlockMove}
|
|
||||||
showAllBlocks={showAllBlocks}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
120
web/components/gantt-chart/chart/main-content.tsx
Normal file
120
web/components/gantt-chart/chart/main-content.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
// components
|
||||||
|
import {
|
||||||
|
BiWeekChartView,
|
||||||
|
DayChartView,
|
||||||
|
GanttChartBlocksList,
|
||||||
|
GanttChartSidebar,
|
||||||
|
HourChartView,
|
||||||
|
IBlockUpdateData,
|
||||||
|
IGanttBlock,
|
||||||
|
MonthChartView,
|
||||||
|
QuarterChartView,
|
||||||
|
TGanttViews,
|
||||||
|
WeekChartView,
|
||||||
|
YearChartView,
|
||||||
|
useChart,
|
||||||
|
} from "components/gantt-chart";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
blocks: IGanttBlock[] | null;
|
||||||
|
blockToRender: (data: any) => React.ReactNode;
|
||||||
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
|
bottomSpacing: boolean;
|
||||||
|
chartBlocks: IGanttBlock[] | null;
|
||||||
|
enableBlockLeftResize: boolean;
|
||||||
|
enableBlockMove: boolean;
|
||||||
|
enableBlockRightResize: boolean;
|
||||||
|
enableReorder: boolean;
|
||||||
|
itemsContainerWidth: number;
|
||||||
|
showAllBlocks: boolean;
|
||||||
|
sidebarToRender: (props: any) => React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
updateCurrentViewRenderPayload: (direction: "left" | "right", currentView: TGanttViews) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GanttChartMainContent: React.FC<Props> = (props) => {
|
||||||
|
const {
|
||||||
|
blocks,
|
||||||
|
blockToRender,
|
||||||
|
blockUpdateHandler,
|
||||||
|
bottomSpacing,
|
||||||
|
chartBlocks,
|
||||||
|
enableBlockLeftResize,
|
||||||
|
enableBlockMove,
|
||||||
|
enableBlockRightResize,
|
||||||
|
enableReorder,
|
||||||
|
itemsContainerWidth,
|
||||||
|
showAllBlocks,
|
||||||
|
sidebarToRender,
|
||||||
|
title,
|
||||||
|
updateCurrentViewRenderPayload,
|
||||||
|
} = props;
|
||||||
|
// chart hook
|
||||||
|
const { currentView, currentViewData, updateScrollLeft } = useChart();
|
||||||
|
// handling scroll functionality
|
||||||
|
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||||
|
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
|
||||||
|
|
||||||
|
updateScrollLeft(scrollLeft);
|
||||||
|
|
||||||
|
const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth;
|
||||||
|
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
|
||||||
|
|
||||||
|
if (approxRangeRight < 1000) updateCurrentViewRenderPayload("right", currentView);
|
||||||
|
if (approxRangeLeft < 1000) updateCurrentViewRenderPayload("left", currentView);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CHART_VIEW_COMPONENTS: {
|
||||||
|
[key in TGanttViews]: React.FC;
|
||||||
|
} = {
|
||||||
|
hours: HourChartView,
|
||||||
|
day: DayChartView,
|
||||||
|
week: WeekChartView,
|
||||||
|
bi_week: BiWeekChartView,
|
||||||
|
month: MonthChartView,
|
||||||
|
quarter: QuarterChartView,
|
||||||
|
year: YearChartView,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!currentView) return null;
|
||||||
|
const ActiveChartView = CHART_VIEW_COMPONENTS[currentView];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
// DO NOT REMOVE THE ID
|
||||||
|
id="gantt-container"
|
||||||
|
className={cn(
|
||||||
|
"h-full w-full overflow-auto horizontal-scroll-enable flex border-t-[0.5px] border-custom-border-200",
|
||||||
|
{
|
||||||
|
"mb-8": bottomSpacing,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onScroll={onScroll}
|
||||||
|
>
|
||||||
|
<GanttChartSidebar
|
||||||
|
blocks={blocks}
|
||||||
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
|
enableReorder={enableReorder}
|
||||||
|
sidebarToRender={sidebarToRender}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
||||||
|
<ActiveChartView />
|
||||||
|
{currentViewData && (
|
||||||
|
<GanttChartBlocksList
|
||||||
|
itemsContainerWidth={itemsContainerWidth}
|
||||||
|
blocks={chartBlocks}
|
||||||
|
blockToRender={blockToRender}
|
||||||
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
|
enableBlockLeftResize={enableBlockLeftResize}
|
||||||
|
enableBlockRightResize={enableBlockRightResize}
|
||||||
|
enableBlockMove={enableBlockMove}
|
||||||
|
showAllBlocks={showAllBlocks}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,72 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
// hooks
|
|
||||||
import { useChart } from "../hooks";
|
|
||||||
// types
|
|
||||||
import { IMonthBlock } from "../views";
|
|
||||||
|
|
||||||
export const MonthChartView: FC<any> = () => {
|
|
||||||
const { currentViewData, renderView } = useChart();
|
|
||||||
|
|
||||||
const monthBlocks: IMonthBlock[] = renderView;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-100/50">
|
|
||||||
{monthBlocks &&
|
|
||||||
monthBlocks.length > 0 &&
|
|
||||||
monthBlocks.map((block, _idxRoot) => (
|
|
||||||
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
|
|
||||||
<div className="h-[60px] w-full">
|
|
||||||
<div className="relative h-[30px]">
|
|
||||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-3 py-2 text-xs font-medium capitalize">
|
|
||||||
{block?.title}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex h-[30px] w-full">
|
|
||||||
{block?.children &&
|
|
||||||
block?.children.length > 0 &&
|
|
||||||
block?.children.map((monthDay, _idx) => (
|
|
||||||
<div
|
|
||||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
|
||||||
className="flex-shrink-0 border-b border-custom-border-200 py-1 text-center capitalize"
|
|
||||||
style={{ width: `${currentViewData?.data.width}px` }}
|
|
||||||
>
|
|
||||||
<div className="space-x-1 text-xs">
|
|
||||||
<span className="text-custom-text-200">{monthDay.dayData.shortTitle[0]}</span>{" "}
|
|
||||||
<span className={monthDay.today ? "rounded-full bg-custom-primary-100 px-1 text-white" : ""}>
|
|
||||||
{monthDay.day}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex h-full w-full divide-x divide-custom-border-100/50">
|
|
||||||
{block?.children &&
|
|
||||||
block?.children.length > 0 &&
|
|
||||||
block?.children.map((monthDay, _idx) => (
|
|
||||||
<div
|
|
||||||
key={`column-${_idxRoot}-${_idx}`}
|
|
||||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
|
||||||
style={{ width: `${currentViewData?.data.width}px` }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`relative flex h-full w-full flex-1 justify-center ${
|
|
||||||
["sat", "sun"].includes(monthDay?.dayData?.shortTitle || "") ? `bg-custom-background-90` : ``
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{/* {monthDay?.today && (
|
|
||||||
<div className="absolute top-0 bottom-0 w-[1px] bg-red-500" />
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
203
web/components/gantt-chart/chart/root.tsx
Normal file
203
web/components/gantt-chart/chart/root.tsx
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import { FC, useEffect, useState } from "react";
|
||||||
|
// components
|
||||||
|
import { GanttChartHeader, useChart, GanttChartMainContent } from "components/gantt-chart";
|
||||||
|
// views
|
||||||
|
import {
|
||||||
|
generateMonthChart,
|
||||||
|
getNumberOfDaysBetweenTwoDatesInMonth,
|
||||||
|
getMonthChartItemPositionWidthInMonth,
|
||||||
|
} from "../views";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
// types
|
||||||
|
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
|
||||||
|
// data
|
||||||
|
import { currentViewDataWithView } from "../data";
|
||||||
|
// constants
|
||||||
|
import { SIDEBAR_WIDTH } from "../constants";
|
||||||
|
|
||||||
|
type ChartViewRootProps = {
|
||||||
|
border: boolean;
|
||||||
|
title: string;
|
||||||
|
loaderTitle: string;
|
||||||
|
blocks: IGanttBlock[] | null;
|
||||||
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
|
blockToRender: (data: any) => React.ReactNode;
|
||||||
|
sidebarToRender: (props: any) => React.ReactNode;
|
||||||
|
enableBlockLeftResize: boolean;
|
||||||
|
enableBlockRightResize: boolean;
|
||||||
|
enableBlockMove: boolean;
|
||||||
|
enableReorder: boolean;
|
||||||
|
bottomSpacing: boolean;
|
||||||
|
showAllBlocks: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||||
|
const {
|
||||||
|
border,
|
||||||
|
title,
|
||||||
|
blocks = null,
|
||||||
|
loaderTitle,
|
||||||
|
blockUpdateHandler,
|
||||||
|
sidebarToRender,
|
||||||
|
blockToRender,
|
||||||
|
enableBlockLeftResize,
|
||||||
|
enableBlockRightResize,
|
||||||
|
enableBlockMove,
|
||||||
|
enableReorder,
|
||||||
|
bottomSpacing,
|
||||||
|
showAllBlocks,
|
||||||
|
} = props;
|
||||||
|
// states
|
||||||
|
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
|
||||||
|
const [fullScreenMode, setFullScreenMode] = useState(false);
|
||||||
|
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
|
||||||
|
// hooks
|
||||||
|
const { currentView, currentViewData, renderView, dispatch } = useChart();
|
||||||
|
|
||||||
|
// rendering the block structure
|
||||||
|
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
|
||||||
|
blocks
|
||||||
|
? blocks.map((block: any) => ({
|
||||||
|
...block,
|
||||||
|
position: getMonthChartItemPositionWidthInMonth(view, block),
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentViewData || !blocks) return;
|
||||||
|
setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
|
||||||
|
}, [currentViewData, blocks]);
|
||||||
|
|
||||||
|
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
|
||||||
|
const selectedCurrentView: TGanttViews = view;
|
||||||
|
const selectedCurrentViewData: ChartDataType | undefined =
|
||||||
|
selectedCurrentView && selectedCurrentView === currentViewData?.key
|
||||||
|
? currentViewData
|
||||||
|
: currentViewDataWithView(view);
|
||||||
|
|
||||||
|
if (selectedCurrentViewData === undefined) return;
|
||||||
|
|
||||||
|
let currentRender: any;
|
||||||
|
if (selectedCurrentView === "month") currentRender = generateMonthChart(selectedCurrentViewData, side);
|
||||||
|
|
||||||
|
// updating the prevData, currentData and nextData
|
||||||
|
if (currentRender.payload.length > 0) {
|
||||||
|
if (side === "left") {
|
||||||
|
dispatch({
|
||||||
|
type: "PARTIAL_UPDATE",
|
||||||
|
payload: {
|
||||||
|
currentView: selectedCurrentView,
|
||||||
|
currentViewData: currentRender.state,
|
||||||
|
renderView: [...currentRender.payload, ...renderView],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
|
||||||
|
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
|
||||||
|
} else if (side === "right") {
|
||||||
|
dispatch({
|
||||||
|
type: "PARTIAL_UPDATE",
|
||||||
|
payload: {
|
||||||
|
currentView: view,
|
||||||
|
currentViewData: currentRender.state,
|
||||||
|
renderView: [...renderView, ...currentRender.payload],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "PARTIAL_UPDATE",
|
||||||
|
payload: {
|
||||||
|
currentView: view,
|
||||||
|
currentViewData: currentRender.state,
|
||||||
|
renderView: [...currentRender.payload],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setItemsContainerWidth(currentRender.scrollWidth);
|
||||||
|
setTimeout(() => {
|
||||||
|
handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToday = () => updateCurrentViewRenderPayload(null, currentView);
|
||||||
|
|
||||||
|
// handling the scroll positioning from left and right
|
||||||
|
useEffect(() => {
|
||||||
|
handleToday();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updatingCurrentLeftScrollPosition = (width: number) => {
|
||||||
|
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||||
|
if (!scrollContainer) return;
|
||||||
|
|
||||||
|
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
|
||||||
|
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
|
||||||
|
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||||
|
if (!scrollContainer) return;
|
||||||
|
|
||||||
|
const clientVisibleWidth: number = scrollContainer?.clientWidth;
|
||||||
|
let scrollWidth: number = 0;
|
||||||
|
let daysDifference: number = 0;
|
||||||
|
|
||||||
|
// if (currentView === "hours")
|
||||||
|
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
||||||
|
// if (currentView === "day")
|
||||||
|
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
||||||
|
// if (currentView === "week")
|
||||||
|
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
||||||
|
// if (currentView === "bi_week")
|
||||||
|
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
||||||
|
if (currentView === "month")
|
||||||
|
daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
||||||
|
// if (currentView === "quarter")
|
||||||
|
// daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date);
|
||||||
|
// if (currentView === "year")
|
||||||
|
// daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date);
|
||||||
|
|
||||||
|
scrollWidth =
|
||||||
|
daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width) + SIDEBAR_WIDTH / 2;
|
||||||
|
|
||||||
|
scrollContainer.scrollLeft = scrollWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("relative flex flex-col h-full select-none rounded-sm bg-custom-background-100 shadow", {
|
||||||
|
"fixed inset-0 z-[999999] bg-custom-background-100": fullScreenMode,
|
||||||
|
"border-[0.5px] border-custom-border-200": border,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<GanttChartHeader
|
||||||
|
blocks={blocks}
|
||||||
|
fullScreenMode={fullScreenMode}
|
||||||
|
toggleFullScreenMode={() => setFullScreenMode((prevData) => !prevData)}
|
||||||
|
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
|
||||||
|
handleToday={handleToday}
|
||||||
|
loaderTitle={loaderTitle}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
<GanttChartMainContent
|
||||||
|
blocks={blocks}
|
||||||
|
blockToRender={blockToRender}
|
||||||
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
|
bottomSpacing={bottomSpacing}
|
||||||
|
chartBlocks={chartBlocks}
|
||||||
|
enableBlockLeftResize={enableBlockLeftResize}
|
||||||
|
enableBlockMove={enableBlockMove}
|
||||||
|
enableBlockRightResize={enableBlockRightResize}
|
||||||
|
enableReorder={enableReorder}
|
||||||
|
itemsContainerWidth={itemsContainerWidth}
|
||||||
|
showAllBlocks={showAllBlocks}
|
||||||
|
sidebarToRender={sidebarToRender}
|
||||||
|
title={title}
|
||||||
|
updateCurrentViewRenderPayload={updateCurrentViewRenderPayload}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// context
|
// context
|
||||||
import { useChart } from "../hooks";
|
import { useChart } from "components/gantt-chart";
|
||||||
|
|
||||||
export const BiWeekChartView: FC<any> = () => {
|
export const BiWeekChartView: FC<any> = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
@ -1,6 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// context
|
// context
|
||||||
import { useChart } from "../hooks";
|
import { useChart } from "../../hooks";
|
||||||
|
|
||||||
export const DayChartView: FC<any> = () => {
|
export const DayChartView: FC<any> = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
@ -1,6 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// context
|
// context
|
||||||
import { useChart } from "../hooks";
|
import { useChart } from "components/gantt-chart";
|
||||||
|
|
||||||
export const HourChartView: FC<any> = () => {
|
export const HourChartView: FC<any> = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
7
web/components/gantt-chart/chart/views/index.ts
Normal file
7
web/components/gantt-chart/chart/views/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export * from "./bi-week";
|
||||||
|
export * from "./day";
|
||||||
|
export * from "./hours";
|
||||||
|
export * from "./month";
|
||||||
|
export * from "./quarter";
|
||||||
|
export * from "./week";
|
||||||
|
export * from "./year";
|
76
web/components/gantt-chart/chart/views/month.tsx
Normal file
76
web/components/gantt-chart/chart/views/month.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
// hooks
|
||||||
|
import { useChart } from "components/gantt-chart";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
// types
|
||||||
|
import { IMonthBlock } from "../../views";
|
||||||
|
// constants
|
||||||
|
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "components/gantt-chart/constants";
|
||||||
|
|
||||||
|
export const MonthChartView: FC<any> = () => {
|
||||||
|
// chart hook
|
||||||
|
const { currentViewData, renderView } = useChart();
|
||||||
|
const monthBlocks: IMonthBlock[] = renderView;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="absolute top-0 left-0 h-full w-max flex divide-x divide-custom-border-100/50">
|
||||||
|
{monthBlocks?.map((block, rootIndex) => (
|
||||||
|
<div key={`month-${block?.month}-${block?.year}`} className="relative">
|
||||||
|
<div
|
||||||
|
className="w-full sticky top-0 z-[5] bg-custom-background-100"
|
||||||
|
style={{
|
||||||
|
height: `${HEADER_HEIGHT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="h-1/2">
|
||||||
|
<div
|
||||||
|
className="sticky inline-flex whitespace-nowrap px-3 py-2 text-xs font-medium capitalize"
|
||||||
|
style={{
|
||||||
|
left: `${SIDEBAR_WIDTH}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{block?.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="h-1/2 w-full flex">
|
||||||
|
{block?.children?.map((monthDay, index) => (
|
||||||
|
<div
|
||||||
|
key={`sub-title-${rootIndex}-${index}`}
|
||||||
|
className="flex-shrink-0 border-b-[0.5px] border-custom-border-200 py-1 text-center capitalize"
|
||||||
|
style={{ width: `${currentViewData?.data.width}px` }}
|
||||||
|
>
|
||||||
|
<div className="space-x-1 text-xs">
|
||||||
|
<span className="text-custom-text-200">{monthDay.dayData.shortTitle[0]}</span>{" "}
|
||||||
|
<span
|
||||||
|
className={cn({
|
||||||
|
"rounded-full bg-custom-primary-100 px-1 text-white": monthDay.today,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{monthDay.day}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="h-full w-full flex divide-x divide-custom-border-100/50">
|
||||||
|
{block?.children?.map((monthDay, index) => (
|
||||||
|
<div
|
||||||
|
key={`column-${rootIndex}-${index}`}
|
||||||
|
className="h-full overflow-hidden"
|
||||||
|
style={{ width: `${currentViewData?.data.width}px` }}
|
||||||
|
>
|
||||||
|
{["sat", "sun"].includes(monthDay?.dayData?.shortTitle) && (
|
||||||
|
<div className="h-full bg-custom-background-90" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// context
|
// context
|
||||||
import { useChart } from "../hooks";
|
import { useChart } from "../../hooks";
|
||||||
|
|
||||||
export const QuarterChartView: FC<any> = () => {
|
export const QuarterChartView: FC<any> = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
@ -1,6 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// context
|
// context
|
||||||
import { useChart } from "../hooks";
|
import { useChart } from "../../hooks";
|
||||||
|
|
||||||
export const WeekChartView: FC<any> = () => {
|
export const WeekChartView: FC<any> = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
@ -1,6 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// context
|
// context
|
||||||
import { useChart } from "../hooks";
|
import { useChart } from "../../hooks";
|
||||||
|
|
||||||
export const YearChartView: FC<any> = () => {
|
export const YearChartView: FC<any> = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
5
web/components/gantt-chart/constants.ts
Normal file
5
web/components/gantt-chart/constants.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const BLOCK_HEIGHT = 44;
|
||||||
|
|
||||||
|
export const HEADER_HEIGHT = 60;
|
||||||
|
|
||||||
|
export const SIDEBAR_WIDTH = 360;
|
@ -24,6 +24,7 @@ const chartReducer = (state: ChartContextData, action: ChartContextActionPayload
|
|||||||
const initialView = "month";
|
const initialView = "month";
|
||||||
|
|
||||||
export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
// states;
|
||||||
const [state, dispatch] = useState<ChartContextData>({
|
const [state, dispatch] = useState<ChartContextData>({
|
||||||
currentView: initialView,
|
currentView: initialView,
|
||||||
currentViewData: currentViewDataWithView(initialView),
|
currentViewData: currentViewDataWithView(initialView),
|
||||||
@ -31,23 +32,25 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
allViews: allViewsWithData,
|
allViews: allViewsWithData,
|
||||||
activeBlock: null,
|
activeBlock: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [scrollLeft, setScrollLeft] = useState(0);
|
const [scrollLeft, setScrollLeft] = useState(0);
|
||||||
|
|
||||||
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
|
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
|
||||||
const newState = chartReducer(state, action);
|
const newState = chartReducer(state, action);
|
||||||
|
|
||||||
dispatch(() => newState);
|
dispatch(() => newState);
|
||||||
|
|
||||||
return newState;
|
return newState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateScrollLeft = (scrollLeft: number) => {
|
const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft);
|
||||||
setScrollLeft(scrollLeft);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}>
|
<ChartContext.Provider
|
||||||
|
value={{
|
||||||
|
...state,
|
||||||
|
scrollLeft,
|
||||||
|
updateScrollLeft,
|
||||||
|
dispatch: handleDispatch,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</ChartContext.Provider>
|
</ChartContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useChart } from "../hooks";
|
import { IGanttBlock, useChart } from "components/gantt-chart";
|
||||||
// types
|
// helpers
|
||||||
import { IGanttBlock } from "../types";
|
import { cn } from "helpers/common.helper";
|
||||||
|
// constants
|
||||||
|
import { SIDEBAR_WIDTH } from "../constants";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: IGanttBlock;
|
block: IGanttBlock;
|
||||||
@ -20,7 +22,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
|||||||
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 [posFromLeft, setPosFromLeft] = useState<number | null>(null);
|
const [isHidden, setIsHidden] = useState(true);
|
||||||
// refs
|
// refs
|
||||||
const resizableRef = useRef<HTMLDivElement>(null);
|
const resizableRef = useRef<HTMLDivElement>(null);
|
||||||
// chart hook
|
// chart hook
|
||||||
@ -31,12 +33,10 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
let delWidth = 0;
|
let delWidth = 0;
|
||||||
|
|
||||||
const ganttContainer = document.querySelector("#gantt-container") as HTMLElement;
|
const ganttContainer = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||||
const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLElement;
|
const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLDivElement;
|
||||||
|
|
||||||
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
|
if (!ganttContainer || !ganttSidebar) return 0;
|
||||||
|
|
||||||
if (!ganttContainer || !ganttSidebar || !scrollContainer) return 0;
|
|
||||||
|
|
||||||
const posFromLeft = e.clientX;
|
const posFromLeft = e.clientX;
|
||||||
// manually scroll to left if reached the left end while dragging
|
// manually scroll to left if reached the left end while dragging
|
||||||
@ -45,7 +45,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
delWidth = -5;
|
delWidth = -5;
|
||||||
|
|
||||||
scrollContainer.scrollBy(delWidth, 0);
|
ganttContainer.scrollBy(delWidth, 0);
|
||||||
} else delWidth = e.movementX;
|
} else delWidth = e.movementX;
|
||||||
|
|
||||||
// manually scroll to right if reached the right end while dragging
|
// manually scroll to right if reached the right end while dragging
|
||||||
@ -55,7 +55,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
delWidth = 5;
|
delWidth = 5;
|
||||||
|
|
||||||
scrollContainer.scrollBy(delWidth, 0);
|
ganttContainer.scrollBy(delWidth, 0);
|
||||||
} else delWidth = e.movementX;
|
} else delWidth = e.movementX;
|
||||||
|
|
||||||
return delWidth;
|
return delWidth;
|
||||||
@ -201,50 +201,61 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
// scroll to a hidden block
|
// scroll to a hidden block
|
||||||
const handleScrollToBlock = () => {
|
const handleScrollToBlock = () => {
|
||||||
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
|
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||||
|
|
||||||
if (!scrollContainer || !block.position) return;
|
if (!scrollContainer || !block.position) return;
|
||||||
|
|
||||||
// update container's scroll position to the block's position
|
// update container's scroll position to the block's position
|
||||||
scrollContainer.scrollLeft = block.position.marginLeft - 4;
|
scrollContainer.scrollLeft = block.position.marginLeft - 4;
|
||||||
};
|
};
|
||||||
// update block position from viewport's left end on scroll
|
|
||||||
useEffect(() => {
|
|
||||||
const block = resizableRef.current;
|
|
||||||
|
|
||||||
if (!block) return;
|
|
||||||
|
|
||||||
setPosFromLeft(block.getBoundingClientRect().left);
|
|
||||||
}, [scrollLeft]);
|
|
||||||
// check if block is hidden on either side
|
// check if block is hidden on either side
|
||||||
const isBlockHiddenOnLeft =
|
const isBlockHiddenOnLeft =
|
||||||
block.position?.marginLeft &&
|
block.position?.marginLeft &&
|
||||||
block.position?.width &&
|
block.position?.width &&
|
||||||
scrollLeft > block.position.marginLeft + block.position.width;
|
scrollLeft > block.position.marginLeft + block.position.width;
|
||||||
const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth;
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||||
|
const resizableBlock = resizableRef.current;
|
||||||
|
if (!resizableBlock || !intersectionRoot) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
setIsHidden(!entry.isIntersecting);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: intersectionRoot,
|
||||||
|
rootMargin: `0px 0px 0px -${SIDEBAR_WIDTH}px`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(resizableBlock);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(resizableBlock);
|
||||||
|
};
|
||||||
|
}, [block.data.name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* move to left side hidden block button */}
|
{/* move to the hidden block */}
|
||||||
{isBlockHiddenOnLeft && (
|
{isHidden && (
|
||||||
<div
|
<button
|
||||||
className="fixed z-[1] ml-1 mt-1.5 grid h-8 w-8 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
type="button"
|
||||||
|
className="sticky z-[1] grid h-8 w-8 translate-y-1.5 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
style={{
|
||||||
|
left: `${SIDEBAR_WIDTH + 4}px`,
|
||||||
|
}}
|
||||||
onClick={handleScrollToBlock}
|
onClick={handleScrollToBlock}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-3.5 w-3.5" />
|
<ArrowRight
|
||||||
</div>
|
className={cn("h-3.5 w-3.5", {
|
||||||
)}
|
"rotate-180": isBlockHiddenOnLeft,
|
||||||
{/* move to right side hidden block button */}
|
})}
|
||||||
{isBlockHiddenOnRight && (
|
/>
|
||||||
<div
|
</button>
|
||||||
className="fixed right-1 z-[1] mt-1.5 grid h-8 w-8 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
onClick={handleScrollToBlock}
|
|
||||||
>
|
|
||||||
<ArrowRight className="h-3.5 w-3.5" />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
id={`block-${block.id}`}
|
|
||||||
ref={resizableRef}
|
ref={resizableRef}
|
||||||
className="group relative inline-flex h-full cursor-pointer items-center font-medium transition-all"
|
className="group relative inline-flex h-full cursor-pointer items-center font-medium transition-all"
|
||||||
style={{
|
style={{
|
||||||
@ -259,17 +270,22 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
|||||||
onMouseDown={handleBlockLeftResize}
|
onMouseDown={handleBlockLeftResize}
|
||||||
onMouseEnter={() => setIsLeftResizing(true)}
|
onMouseEnter={() => setIsLeftResizing(true)}
|
||||||
onMouseLeave={() => setIsLeftResizing(false)}
|
onMouseLeave={() => setIsLeftResizing(false)}
|
||||||
className="absolute -left-2.5 top-1/2 z-[3] h-full w-6 -translate-y-1/2 cursor-col-resize rounded-md"
|
className="absolute -left-2.5 top-1/2 -translate-y-1/2 z-[3] h-full w-6 cursor-col-resize rounded-md"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1/2 h-7 w-1 -translate-y-1/2 rounded-sm bg-custom-background-100 transition-all duration-300 ${
|
className={cn(
|
||||||
isLeftResizing ? "-left-2.5" : "left-1"
|
"absolute left-1 top-1/2 -translate-y-1/2 h-7 w-1 rounded-sm bg-custom-background-100 transition-all duration-300",
|
||||||
}`}
|
{
|
||||||
|
"-left-2.5": isLeftResizing,
|
||||||
|
}
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`relative z-[2] flex h-8 w-full items-center rounded ${isMoving ? "pointer-events-none" : ""}`}
|
className={cn("relative z-[2] flex h-8 w-full items-center rounded", {
|
||||||
|
"pointer-events-none": isMoving,
|
||||||
|
})}
|
||||||
onMouseDown={handleBlockMove}
|
onMouseDown={handleBlockMove}
|
||||||
>
|
>
|
||||||
{blockToRender(block.data)}
|
{blockToRender(block.data)}
|
||||||
@ -281,12 +297,15 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
|||||||
onMouseDown={handleBlockRightResize}
|
onMouseDown={handleBlockRightResize}
|
||||||
onMouseEnter={() => setIsRightResizing(true)}
|
onMouseEnter={() => setIsRightResizing(true)}
|
||||||
onMouseLeave={() => setIsRightResizing(false)}
|
onMouseLeave={() => setIsRightResizing(false)}
|
||||||
className="absolute -right-2.5 top-1/2 z-[2] h-full w-6 -translate-y-1/2 cursor-col-resize rounded-md"
|
className="absolute -right-2.5 top-1/2 -translate-y-1/2 z-[2] h-full w-6 cursor-col-resize rounded-md"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1/2 h-7 w-1 -translate-y-1/2 rounded-sm bg-custom-background-100 transition-all duration-300 ${
|
className={cn(
|
||||||
isRightResizing ? "-right-2.5" : "right-1"
|
"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-1 rounded-sm bg-custom-background-100 transition-all duration-300",
|
||||||
}`}
|
{
|
||||||
|
"-right-2.5": isRightResizing,
|
||||||
|
}
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./blocks";
|
export * from "./blocks";
|
||||||
|
export * from "./chart";
|
||||||
export * from "./helpers";
|
export * from "./helpers";
|
||||||
export * from "./hooks";
|
export * from "./hooks";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// components
|
// components
|
||||||
import { ChartViewRoot } from "./chart";
|
import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
|
||||||
// context
|
// context
|
||||||
import { ChartContextProvider } from "./contexts";
|
import { ChartContextProvider } from "./contexts";
|
||||||
// types
|
|
||||||
import { IBlockUpdateData, IGanttBlock } from "./types";
|
|
||||||
|
|
||||||
type GanttChartRootProps = {
|
type GanttChartRootProps = {
|
||||||
border?: boolean;
|
border?: boolean;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
|
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
|
||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -9,8 +8,11 @@ import { Loader } from "@plane/ui";
|
|||||||
import { CycleGanttSidebarBlock } from "components/cycles";
|
import { CycleGanttSidebarBlock } from "components/cycles";
|
||||||
// helpers
|
// helpers
|
||||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||||
|
import { cn } from "helpers/common.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;
|
||||||
@ -20,12 +22,8 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const { blockUpdateHandler, blocks, enableReorder } = props;
|
||||||
const { title, blockUpdateHandler, blocks, enableReorder } = props;
|
// chart hook
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { cycleId } = router.query;
|
|
||||||
|
|
||||||
const { activeBlock, dispatch } = useChart();
|
const { activeBlock, dispatch } = useChart();
|
||||||
|
|
||||||
// update the active block on hover
|
// update the active block on hover
|
||||||
@ -84,12 +82,7 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
<DragDropContext onDragEnd={handleOrderChange}>
|
<DragDropContext onDragEnd={handleOrderChange}>
|
||||||
<Droppable droppableId="gantt-sidebar">
|
<Droppable droppableId="gantt-sidebar">
|
||||||
{(droppableProvided) => (
|
{(droppableProvided) => (
|
||||||
<div
|
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
||||||
id={`gantt-sidebar-${cycleId}`}
|
|
||||||
className="mt-3 max-h-full overflow-y-auto pl-2.5"
|
|
||||||
ref={droppableProvided.innerRef}
|
|
||||||
{...droppableProvided.droppableProps}
|
|
||||||
>
|
|
||||||
<>
|
<>
|
||||||
{blocks ? (
|
{blocks ? (
|
||||||
blocks.map((block, index) => {
|
blocks.map((block, index) => {
|
||||||
@ -104,7 +97,9 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
>
|
>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`h-11 ${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
|
className={cn({
|
||||||
|
"rounded bg-custom-background-80": snapshot.isDragging,
|
||||||
|
})}
|
||||||
onMouseEnter={() => updateActiveBlock(block)}
|
onMouseEnter={() => updateActiveBlock(block)}
|
||||||
onMouseLeave={() => updateActiveBlock(null)}
|
onMouseLeave={() => updateActiveBlock(null)}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
@ -112,9 +107,12 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id={`sidebar-block-${block.id}`}
|
id={`sidebar-block-${block.id}`}
|
||||||
className={`group flex h-full w-full items-center gap-2 rounded-l px-2 pr-4 ${
|
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
|
||||||
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
|
"bg-custom-background-80": activeBlock?.id === block.id,
|
||||||
}`}
|
})}
|
||||||
|
style={{
|
||||||
|
height: `${BLOCK_HEIGHT}px`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{enableReorder && (
|
{enableReorder && (
|
||||||
<button
|
<button
|
||||||
@ -128,9 +126,9 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
|
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
|
||||||
<div className="flex-grow truncate">
|
<div className="flex-grow truncate">
|
||||||
<CycleGanttSidebarBlock data={block.data} />
|
<CycleGanttSidebarBlock cycleId={block.data.id} />
|
||||||
</div>
|
</div>
|
||||||
{duration !== undefined && (
|
{duration && (
|
||||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||||
{duration} day{duration > 1 ? "s" : ""}
|
{duration} day{duration > 1 ? "s" : ""}
|
||||||
</div>
|
</div>
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./cycle-sidebar";
|
export * from "./cycles";
|
||||||
export * from "./module-sidebar";
|
export * from "./issues";
|
||||||
export * from "./sidebar";
|
export * from "./modules";
|
||||||
export * from "./project-view-sidebar";
|
export * from "./project-views";
|
||||||
|
export * from "./root";
|
||||||
|
195
web/components/gantt-chart/sidebar/issues.tsx
Normal file
195
web/components/gantt-chart/sidebar/issues.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
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 { GanttQuickAddIssueForm, 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 { TIssue } from "@plane/types";
|
||||||
|
import { BLOCK_HEIGHT } from "../constants";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
|
blocks: IGanttBlock[] | null;
|
||||||
|
enableReorder: boolean;
|
||||||
|
enableQuickIssueCreate?: boolean;
|
||||||
|
quickAddCallback?: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
data: TIssue,
|
||||||
|
viewId?: string
|
||||||
|
) => Promise<TIssue | undefined>;
|
||||||
|
viewId?: string;
|
||||||
|
disableIssueCreation?: boolean;
|
||||||
|
showAllBlocks?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
|
||||||
|
const {
|
||||||
|
blockUpdateHandler,
|
||||||
|
blocks,
|
||||||
|
enableReorder,
|
||||||
|
enableQuickIssueCreate,
|
||||||
|
quickAddCallback,
|
||||||
|
viewId,
|
||||||
|
disableIssueCreation,
|
||||||
|
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>
|
||||||
|
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||||
|
<GanttQuickAddIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -1,4 +1,3 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -9,8 +8,11 @@ import { Loader } from "@plane/ui";
|
|||||||
import { ModuleGanttSidebarBlock } from "components/modules";
|
import { ModuleGanttSidebarBlock } from "components/modules";
|
||||||
// helpers
|
// helpers
|
||||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
|
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
|
||||||
|
// constants
|
||||||
|
import { BLOCK_HEIGHT } from "../constants";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
@ -20,12 +22,8 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const { blockUpdateHandler, blocks, enableReorder } = props;
|
||||||
const { title, blockUpdateHandler, blocks, enableReorder } = props;
|
// chart hook
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { cycleId } = router.query;
|
|
||||||
|
|
||||||
const { activeBlock, dispatch } = useChart();
|
const { activeBlock, dispatch } = useChart();
|
||||||
|
|
||||||
// update the active block on hover
|
// update the active block on hover
|
||||||
@ -84,12 +82,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
<DragDropContext onDragEnd={handleOrderChange}>
|
<DragDropContext onDragEnd={handleOrderChange}>
|
||||||
<Droppable droppableId="gantt-sidebar">
|
<Droppable droppableId="gantt-sidebar">
|
||||||
{(droppableProvided) => (
|
{(droppableProvided) => (
|
||||||
<div
|
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
||||||
id={`gantt-sidebar-${cycleId}`}
|
|
||||||
className="mt-3 max-h-full overflow-y-auto pl-2.5"
|
|
||||||
ref={droppableProvided.innerRef}
|
|
||||||
{...droppableProvided.droppableProps}
|
|
||||||
>
|
|
||||||
<>
|
<>
|
||||||
{blocks ? (
|
{blocks ? (
|
||||||
blocks.map((block, index) => {
|
blocks.map((block, index) => {
|
||||||
@ -104,7 +97,9 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
>
|
>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`h-11 ${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
|
className={cn({
|
||||||
|
"rounded bg-custom-background-80": snapshot.isDragging,
|
||||||
|
})}
|
||||||
onMouseEnter={() => updateActiveBlock(block)}
|
onMouseEnter={() => updateActiveBlock(block)}
|
||||||
onMouseLeave={() => updateActiveBlock(null)}
|
onMouseLeave={() => updateActiveBlock(null)}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
@ -112,9 +107,12 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id={`sidebar-block-${block.id}`}
|
id={`sidebar-block-${block.id}`}
|
||||||
className={`group flex h-full w-full items-center gap-2 rounded-l px-2 pr-4 ${
|
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
|
||||||
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
|
"bg-custom-background-80": activeBlock?.id === block.id,
|
||||||
}`}
|
})}
|
||||||
|
style={{
|
||||||
|
height: `${BLOCK_HEIGHT}px`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{enableReorder && (
|
{enableReorder && (
|
||||||
<button
|
<button
|
||||||
@ -128,7 +126,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
|
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
|
||||||
<div className="flex-grow truncate">
|
<div className="flex-grow truncate">
|
||||||
<ModuleGanttSidebarBlock data={block.data} />
|
<ModuleGanttSidebarBlock moduleId={block.data.id} />
|
||||||
</div>
|
</div>
|
||||||
{duration !== undefined && (
|
{duration !== undefined && (
|
||||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
@ -1,4 +1,3 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -11,6 +10,8 @@ import { IssueGanttSidebarBlock } from "components/issues";
|
|||||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
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;
|
||||||
@ -21,12 +22,8 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const { blockUpdateHandler, blocks, enableReorder } = props;
|
||||||
const { title, blockUpdateHandler, blocks, enableReorder } = props;
|
// chart hook
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { cycleId } = router.query;
|
|
||||||
|
|
||||||
const { activeBlock, dispatch } = useChart();
|
const { activeBlock, dispatch } = useChart();
|
||||||
|
|
||||||
// update the active block on hover
|
// update the active block on hover
|
||||||
@ -86,7 +83,6 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
<Droppable droppableId="gantt-sidebar">
|
<Droppable droppableId="gantt-sidebar">
|
||||||
{(droppableProvided) => (
|
{(droppableProvided) => (
|
||||||
<div
|
<div
|
||||||
id={`gantt-sidebar-${cycleId}`}
|
|
||||||
className="mt-3 max-h-full overflow-y-auto pl-2.5"
|
className="mt-3 max-h-full overflow-y-auto pl-2.5"
|
||||||
ref={droppableProvided.innerRef}
|
ref={droppableProvided.innerRef}
|
||||||
{...droppableProvided.droppableProps}
|
{...droppableProvided.droppableProps}
|
||||||
@ -105,7 +101,10 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
>
|
>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`h-11 ${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
|
className={`${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
|
||||||
|
style={{
|
||||||
|
height: `${BLOCK_HEIGHT}px`,
|
||||||
|
}}
|
||||||
onMouseEnter={() => updateActiveBlock(block)}
|
onMouseEnter={() => updateActiveBlock(block)}
|
||||||
onMouseLeave={() => updateActiveBlock(null)}
|
onMouseLeave={() => updateActiveBlock(null)}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
@ -129,7 +128,7 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
|
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
|
||||||
<div className="flex-grow truncate">
|
<div className="flex-grow truncate">
|
||||||
<IssueGanttSidebarBlock data={block.data} />
|
<IssueGanttSidebarBlock issueId={block.data.id} />
|
||||||
</div>
|
</div>
|
||||||
{duration !== undefined && (
|
{duration !== undefined && (
|
||||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
41
web/components/gantt-chart/sidebar/root.tsx
Normal file
41
web/components/gantt-chart/sidebar/root.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// components
|
||||||
|
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
|
||||||
|
// constants
|
||||||
|
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
blocks: IGanttBlock[] | null;
|
||||||
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
|
enableReorder: boolean;
|
||||||
|
sidebarToRender: (props: any) => React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GanttChartSidebar: React.FC<Props> = (props) => {
|
||||||
|
const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
// DO NOT REMOVE THE ID
|
||||||
|
id="gantt-sidebar"
|
||||||
|
className="sticky left-0 z-10 min-h-full h-max flex-shrink-0 border-r-[0.5px] border-custom-border-200"
|
||||||
|
style={{
|
||||||
|
width: `${SIDEBAR_WIDTH}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="box-border flex-shrink-0 flex items-end justify-between gap-2 border-b-[0.5px] border-custom-border-200 pb-2 pl-8 pr-4 text-sm font-medium text-custom-text-300 sticky top-0 z-10 bg-custom-background-100"
|
||||||
|
style={{
|
||||||
|
height: `${HEADER_HEIGHT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h6>{title}</h6>
|
||||||
|
<h6>Duration</h6>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-h-full h-max bg-custom-background-100">
|
||||||
|
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,198 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
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 { GanttQuickAddIssueForm, 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 { TIssue } from "@plane/types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
|
||||||
blocks: IGanttBlock[] | null;
|
|
||||||
enableReorder: boolean;
|
|
||||||
enableQuickIssueCreate?: boolean;
|
|
||||||
quickAddCallback?: (
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
data: TIssue,
|
|
||||||
viewId?: string
|
|
||||||
) => Promise<TIssue | undefined>;
|
|
||||||
viewId?: string;
|
|
||||||
disableIssueCreation?: boolean;
|
|
||||||
showAllBlocks?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
|
||||||
const {
|
|
||||||
blockUpdateHandler,
|
|
||||||
blocks,
|
|
||||||
enableReorder,
|
|
||||||
enableQuickIssueCreate,
|
|
||||||
quickAddCallback,
|
|
||||||
viewId,
|
|
||||||
disableIssueCreation,
|
|
||||||
showAllBlocks = false,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { cycleId } = router.query;
|
|
||||||
|
|
||||||
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
|
|
||||||
id={`gantt-sidebar-${cycleId}`}
|
|
||||||
className="mt-[12px] max-h-full overflow-y-auto pl-2.5"
|
|
||||||
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 = 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(
|
|
||||||
"h-11",
|
|
||||||
{ "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
|
|
||||||
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 data={block.data} />
|
|
||||||
</div>
|
|
||||||
{duration !== undefined && (
|
|
||||||
<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}
|
|
||||||
</>
|
|
||||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
|
||||||
<GanttQuickAddIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
);
|
|
||||||
};
|
|
@ -69,7 +69,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||||||
loaderTitle="Issues"
|
loaderTitle="Issues"
|
||||||
blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null}
|
blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null}
|
||||||
blockUpdateHandler={updateIssueBlockStructure}
|
blockUpdateHandler={updateIssueBlockStructure}
|
||||||
blockToRender={(data: TIssue) => <IssueGanttBlock data={data} />}
|
blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} />}
|
||||||
sidebarToRender={(props) => (
|
sidebarToRender={(props) => (
|
||||||
<IssueGanttSidebar
|
<IssueGanttSidebar
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -1,33 +1,41 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
|
// hooks
|
||||||
|
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
|
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
// types
|
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
|
|
||||||
|
|
||||||
export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
|
type Props = {
|
||||||
// hooks
|
issueId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueGanttBlock: React.FC<Props> = observer((props) => {
|
||||||
|
const { issueId } = props;
|
||||||
|
// store hooks
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug },
|
router: { workspaceSlug },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { getProjectStates } = useProjectState();
|
const { getProjectStates } = useProjectState();
|
||||||
const { setPeekIssue } = useIssueDetail();
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
setPeekIssue,
|
||||||
|
} = useIssueDetail();
|
||||||
|
// derived values
|
||||||
|
const issueDetails = getIssueById(issueId);
|
||||||
|
const stateDetails =
|
||||||
|
issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id);
|
||||||
|
|
||||||
const handleIssuePeekOverview = () =>
|
const handleIssuePeekOverview = () =>
|
||||||
workspaceSlug &&
|
workspaceSlug &&
|
||||||
data &&
|
issueDetails &&
|
||||||
data.project_id &&
|
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
|
||||||
data.id &&
|
|
||||||
setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id });
|
|
||||||
|
|
||||||
const stateColor = getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id)?.color || "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full cursor-pointer items-center rounded"
|
className="relative flex h-full w-full cursor-pointer items-center rounded"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: stateColor,
|
backgroundColor: stateDetails?.color,
|
||||||
}}
|
}}
|
||||||
onClick={handleIssuePeekOverview}
|
onClick={handleIssuePeekOverview}
|
||||||
>
|
>
|
||||||
@ -35,58 +43,62 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h5>{data?.name}</h5>
|
<h5>{issueDetails?.name}</h5>
|
||||||
<div>
|
<div>
|
||||||
{renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.target_date ?? "")}
|
{renderFormattedDate(issueDetails?.start_date ?? "")} to{" "}
|
||||||
|
{renderFormattedDate(issueDetails?.target_date ?? "")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
position="top-left"
|
position="top-left"
|
||||||
>
|
>
|
||||||
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{data?.name}</div>
|
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100 overflow-hidden">
|
||||||
|
{issueDetails?.name}
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
// rendering issues on gantt sidebar
|
// rendering issues on gantt sidebar
|
||||||
export const IssueGanttSidebarBlock = ({ data }: { data: TIssue }) => {
|
export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
||||||
// hooks
|
const { issueId } = props;
|
||||||
const { getProjectStates } = useProjectState();
|
// store hooks
|
||||||
|
const { getStateById } = useProjectState();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug },
|
router: { workspaceSlug },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { setPeekIssue } = useIssueDetail();
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
setPeekIssue,
|
||||||
|
} = useIssueDetail();
|
||||||
|
// derived values
|
||||||
|
const issueDetails = getIssueById(issueId);
|
||||||
|
const projectDetails = issueDetails && getProjectById(issueDetails?.project_id);
|
||||||
|
const stateDetails = issueDetails && getStateById(issueDetails?.state_id);
|
||||||
|
|
||||||
const handleIssuePeekOverview = () =>
|
const handleIssuePeekOverview = () =>
|
||||||
workspaceSlug &&
|
workspaceSlug &&
|
||||||
data &&
|
issueDetails &&
|
||||||
data.project_id &&
|
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
|
||||||
data.id &&
|
|
||||||
setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id });
|
|
||||||
|
|
||||||
const currentStateDetails =
|
|
||||||
getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id) || undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlLink
|
<ControlLink
|
||||||
href={`/${workspaceSlug}/projects/${data.project_id}/issues/${data.id}`}
|
href={`/${workspaceSlug}/projects/${issueDetails?.project_id}/issues/${issueDetails?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={handleIssuePeekOverview}
|
onClick={handleIssuePeekOverview}
|
||||||
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||||
>
|
>
|
||||||
<div className="relative flex h-full w-full cursor-pointer items-center gap-2" onClick={handleIssuePeekOverview}>
|
<div className="relative flex h-full w-full cursor-pointer items-center gap-2" onClick={handleIssuePeekOverview}>
|
||||||
{currentStateDetails != undefined && (
|
{stateDetails && <StateGroupIcon stateGroup={stateDetails?.group} color={stateDetails?.color} />}
|
||||||
<StateGroupIcon stateGroup={currentStateDetails?.group} color={currentStateDetails?.color} />
|
|
||||||
)}
|
|
||||||
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
||||||
{getProjectById(data?.project_id)?.identifier} {data?.sequence_id}
|
{projectDetails?.identifier} {issueDetails?.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={data.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={issueDetails?.name}>
|
||||||
<span className="flex-grow truncate text-sm font-medium">{data?.name}</span>
|
<span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</ControlLink>
|
</ControlLink>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -11,6 +11,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
import { createIssuePayload } from "helpers/issue.helper";
|
import { createIssuePayload } from "helpers/issue.helper";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { IProject, TIssue } from "@plane/types";
|
import { IProject, TIssue } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
@ -138,10 +139,12 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
{isOpen ? (
|
||||||
className={`${errors && errors?.name && errors?.name?.message ? `border border-red-500/20 bg-red-500/10` : ``}`}
|
<div
|
||||||
>
|
className={cn("sticky bottom-0 z-[1] bg-custom-background-100", {
|
||||||
{isOpen ? (
|
"border border-red-500/20 bg-red-500/10": errors && errors?.name && errors?.name?.message,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div className="shadow-custom-shadow-sm">
|
<div className="shadow-custom-shadow-sm">
|
||||||
<form
|
<form
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -152,16 +155,17 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
|
|||||||
</form>
|
</form>
|
||||||
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
|
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div
|
) : (
|
||||||
className="flex w-full cursor-pointer items-center gap-2 p-3 py-3 text-custom-primary-100"
|
<button
|
||||||
onClick={() => setIsOpen(true)}
|
type="button"
|
||||||
>
|
className="sticky bottom-0 z-[1] flex w-full cursor-pointer items-center gap-2 p-3 py-3 text-custom-primary-100 bg-custom-background-100"
|
||||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
onClick={() => setIsOpen(true)}
|
||||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
>
|
||||||
</div>
|
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||||
)}
|
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||||
</div>
|
</button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,52 +1,74 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// hooks
|
||||||
|
import { useApplication, useModule } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, ModuleStatusIcon } from "@plane/ui";
|
import { Tooltip, ModuleStatusIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
// types
|
|
||||||
import { IModule } from "@plane/types";
|
|
||||||
// constants
|
// constants
|
||||||
import { MODULE_STATUS } from "constants/module";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
|
|
||||||
export const ModuleGanttBlock = ({ data }: { data: IModule }) => {
|
type Props = {
|
||||||
|
moduleId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
|
||||||
|
const { moduleId } = props;
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
// store hooks
|
||||||
|
const {
|
||||||
|
router: { workspaceSlug },
|
||||||
|
} = useApplication();
|
||||||
|
const { getModuleById } = useModule();
|
||||||
|
// derived values
|
||||||
|
const moduleDetails = getModuleById(moduleId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full items-center rounded"
|
className="relative flex h-full w-full items-center rounded"
|
||||||
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === data?.status)?.color }}
|
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color }}
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data?.id}`)}
|
onClick={() => router.push(`/${workspaceSlug}/projects/${moduleDetails?.project}/modules/${moduleDetails?.id}`)}
|
||||||
>
|
>
|
||||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h5>{data?.name}</h5>
|
<h5>{moduleDetails?.name}</h5>
|
||||||
<div>
|
<div>
|
||||||
{renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.target_date ?? "")}
|
{renderFormattedDate(moduleDetails?.start_date ?? "")} to{" "}
|
||||||
|
{renderFormattedDate(moduleDetails?.target_date ?? "")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
position="top-left"
|
position="top-left"
|
||||||
>
|
>
|
||||||
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{data?.name}</div>
|
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{moduleDetails?.name}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export const ModuleGanttSidebarBlock = ({ data }: { data: IModule }) => {
|
export const ModuleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
||||||
|
const { moduleId } = props;
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
// store hooks
|
||||||
|
const {
|
||||||
|
router: { workspaceSlug },
|
||||||
|
} = useApplication();
|
||||||
|
const { getModuleById } = useModule();
|
||||||
|
// derived values
|
||||||
|
const moduleDetails = getModuleById(moduleId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full items-center gap-2"
|
className="relative flex h-full w-full items-center gap-2"
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data.id}`)}
|
onClick={() => router.push(`/${workspaceSlug}/projects/${moduleDetails?.project}/modules/${moduleDetails?.id}`)}
|
||||||
>
|
>
|
||||||
<ModuleStatusIcon status={data?.status ?? "backlog"} height="16px" width="16px" />
|
<ModuleStatusIcon status={moduleDetails?.status ?? "backlog"} height="16px" width="16px" />
|
||||||
<h6 className="flex-grow truncate text-sm font-medium">{data.name}</h6>
|
<h6 className="flex-grow truncate text-sm font-medium">{moduleDetails?.name}</h6>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -47,7 +47,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
|||||||
blocks={projectModuleIds ? blockFormat(projectModuleIds) : null}
|
blocks={projectModuleIds ? blockFormat(projectModuleIds) : null}
|
||||||
sidebarToRender={(props) => <ModuleGanttSidebar {...props} />}
|
sidebarToRender={(props) => <ModuleGanttSidebar {...props} />}
|
||||||
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
|
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
|
||||||
blockToRender={(data: IModule) => <ModuleGanttBlock data={data} />}
|
blockToRender={(data: IModule) => <ModuleGanttBlock moduleId={data.id} />}
|
||||||
enableBlockLeftResize={isAllowed}
|
enableBlockLeftResize={isAllowed}
|
||||||
enableBlockRightResize={isAllowed}
|
enableBlockRightResize={isAllowed}
|
||||||
enableBlockMove={isAllowed}
|
enableBlockMove={isAllowed}
|
||||||
|
Loading…
Reference in New Issue
Block a user