From 963d26ccdad56e90dec885065bee420132c6a86c Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:08:17 +0530 Subject: [PATCH] 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 --- web/components/cycles/gantt-chart/blocks.tsx | 55 ++- .../cycles/gantt-chart/cycles-list-layout.tsx | 2 +- .../{blocks-display.tsx => blocks-list.tsx} | 78 +++-- web/components/gantt-chart/blocks/index.ts | 2 +- web/components/gantt-chart/chart/header.tsx | 59 ++++ web/components/gantt-chart/chart/index.ts | 4 + web/components/gantt-chart/chart/index.tsx | 324 ------------------ .../gantt-chart/chart/main-content.tsx | 120 +++++++ web/components/gantt-chart/chart/month.tsx | 72 ---- web/components/gantt-chart/chart/root.tsx | 203 +++++++++++ .../gantt-chart/chart/{ => views}/bi-week.tsx | 2 +- .../gantt-chart/chart/{ => views}/day.tsx | 2 +- .../gantt-chart/chart/{ => views}/hours.tsx | 2 +- .../gantt-chart/chart/views/index.ts | 7 + .../gantt-chart/chart/views/month.tsx | 76 ++++ .../gantt-chart/chart/{ => views}/quarter.tsx | 2 +- .../gantt-chart/chart/{ => views}/week.tsx | 2 +- .../gantt-chart/chart/{ => views}/year.tsx | 2 +- web/components/gantt-chart/constants.ts | 5 + web/components/gantt-chart/contexts/index.tsx | 17 +- ...block-structure.tsx => block-structure.ts} | 0 .../gantt-chart/helpers/draggable.tsx | 117 ++++--- web/components/gantt-chart/index.ts | 1 + web/components/gantt-chart/root.tsx | 4 +- .../sidebar/{cycle-sidebar.tsx => cycles.tsx} | 36 +- web/components/gantt-chart/sidebar/index.ts | 9 +- web/components/gantt-chart/sidebar/issues.tsx | 195 +++++++++++ .../{module-sidebar.tsx => modules.tsx} | 34 +- ...ect-view-sidebar.tsx => project-views.tsx} | 19 +- web/components/gantt-chart/sidebar/root.tsx | 41 +++ .../gantt-chart/sidebar/sidebar.tsx | 198 ----------- .../issue-layouts/gantt/base-gantt-root.tsx | 2 +- .../issues/issue-layouts/gantt/blocks.tsx | 84 +++-- .../gantt/quick-add-issue-form.tsx | 32 +- web/components/modules/gantt-chart/blocks.tsx | 54 ++- .../gantt-chart/modules-list-layout.tsx | 2 +- 36 files changed, 1035 insertions(+), 829 deletions(-) rename web/components/gantt-chart/blocks/{blocks-display.tsx => blocks-list.tsx} (61%) create mode 100644 web/components/gantt-chart/chart/header.tsx create mode 100644 web/components/gantt-chart/chart/index.ts delete mode 100644 web/components/gantt-chart/chart/index.tsx create mode 100644 web/components/gantt-chart/chart/main-content.tsx delete mode 100644 web/components/gantt-chart/chart/month.tsx create mode 100644 web/components/gantt-chart/chart/root.tsx rename web/components/gantt-chart/chart/{ => views}/bi-week.tsx (97%) rename web/components/gantt-chart/chart/{ => views}/day.tsx (98%) rename web/components/gantt-chart/chart/{ => views}/hours.tsx (97%) create mode 100644 web/components/gantt-chart/chart/views/index.ts create mode 100644 web/components/gantt-chart/chart/views/month.tsx rename web/components/gantt-chart/chart/{ => views}/quarter.tsx (98%) rename web/components/gantt-chart/chart/{ => views}/week.tsx (98%) rename web/components/gantt-chart/chart/{ => views}/year.tsx (98%) create mode 100644 web/components/gantt-chart/constants.ts rename web/components/gantt-chart/helpers/{block-structure.tsx => block-structure.ts} (100%) rename web/components/gantt-chart/sidebar/{cycle-sidebar.tsx => cycles.tsx} (85%) create mode 100644 web/components/gantt-chart/sidebar/issues.tsx rename web/components/gantt-chart/sidebar/{module-sidebar.tsx => modules.tsx} (86%) rename web/components/gantt-chart/sidebar/{project-view-sidebar.tsx => project-views.tsx} (92%) create mode 100644 web/components/gantt-chart/sidebar/root.tsx delete mode 100644 web/components/gantt-chart/sidebar/sidebar.tsx diff --git a/web/components/cycles/gantt-chart/blocks.tsx b/web/components/cycles/gantt-chart/blocks.tsx index 46bc04039..beb239d87 100644 --- a/web/components/cycles/gantt-chart/blocks.tsx +++ b/web/components/cycles/gantt-chart/blocks.tsx @@ -1,16 +1,30 @@ import { useRouter } from "next/router"; +import { observer } from "mobx-react"; +// hooks +import { useApplication, useCycle } from "hooks/store"; // ui import { Tooltip, ContrastIcon } from "@plane/ui"; // helpers 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 = observer((props) => { + const { cycleId } = props; + // router 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 (
{ ? "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}`)} >
-
{data?.name}
+
{cycleDetails?.name}
- {renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.end_date ?? "")} + {renderFormattedDate(cycleDetails?.start_date ?? "")} to{" "} + {renderFormattedDate(cycleDetails?.end_date ?? "")}
} position="top-left" > -
{data?.name}
+
{cycleDetails?.name}
); -}; +}); -export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => { +export const CycleGanttSidebarBlock: React.FC = observer((props) => { + const { cycleId } = props; + // router 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 (
router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)} + onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)} > { : "" }`} /> -
{data?.name}
+
{cycleDetails?.name}
); -}; +}); diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 797fc9e39..421a73a4a 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -63,7 +63,7 @@ export const CyclesListGanttChartView: FC = observer((props) => { blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} sidebarToRender={(props) => } - blockToRender={(data: ICycle) => } + blockToRender={(data: ICycle) => } enableBlockLeftResize={false} enableBlockRightResize={false} enableBlockMove={false} diff --git a/web/components/gantt-chart/blocks/blocks-display.tsx b/web/components/gantt-chart/blocks/blocks-list.tsx similarity index 61% rename from web/components/gantt-chart/blocks/blocks-display.tsx rename to web/components/gantt-chart/blocks/blocks-list.tsx index e13be116b..b65f04bb6 100644 --- a/web/components/gantt-chart/blocks/blocks-display.tsx +++ b/web/components/gantt-chart/blocks/blocks-list.tsx @@ -1,3 +1,4 @@ +import { observer } from "mobx-react"; import { FC } from "react"; // hooks import { useIssueDetail } from "hooks/store"; @@ -8,6 +9,8 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { cn } from "helpers/common.helper"; // types import { IBlockUpdateData, IGanttBlock } from "../types"; +// constants +import { BLOCK_HEIGHT, HEADER_HEIGHT } from "../constants"; export type GanttChartBlocksProps = { itemsContainerWidth: number; @@ -20,7 +23,7 @@ export type GanttChartBlocksProps = { showAllBlocks: boolean; }; -export const GanttChartBlocks: FC = (props) => { +export const GanttChartBlocksList: FC = observer((props) => { const { itemsContainerWidth, blocks, @@ -31,9 +34,10 @@ export const GanttChartBlocks: FC = (props) => { enableBlockMove, showAllBlocks, } = props; - - const { activeBlock, dispatch } = useChart(); + // store hooks const { peekIssue } = useIssueDetail(); + // chart hook + const { activeBlock, dispatch } = useChart(); // update the active block on hover const updateActiveBlock = (block: IGanttBlock | null) => { @@ -77,43 +81,51 @@ export const GanttChartBlocks: FC = (props) => { return (
- {blocks && - blocks.length > 0 && - blocks.map((block) => { - // 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; + {blocks?.map((block) => { + // 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 ( +
updateActiveBlock(block)} onMouseLeave={() => updateActiveBlock(null)} > - {!isBlockVisibleOnChart && } - handleChartBlockPosition(block, ...args)} - enableBlockLeftResize={enableBlockLeftResize} - enableBlockRightResize={enableBlockRightResize} - enableBlockMove={enableBlockMove} - /> + {isBlockVisibleOnChart ? ( + handleChartBlockPosition(block, ...args)} + enableBlockLeftResize={enableBlockLeftResize} + enableBlockRightResize={enableBlockRightResize} + enableBlockMove={enableBlockMove} + /> + ) : ( + + )}
- ); - })} +
+ ); + })}
); -}; +}); diff --git a/web/components/gantt-chart/blocks/index.ts b/web/components/gantt-chart/blocks/index.ts index 18ca5da9e..c99f8af32 100644 --- a/web/components/gantt-chart/blocks/index.ts +++ b/web/components/gantt-chart/blocks/index.ts @@ -1 +1 @@ -export * from "./blocks-display"; +export * from "./blocks-list"; diff --git a/web/components/gantt-chart/chart/header.tsx b/web/components/gantt-chart/chart/header.tsx new file mode 100644 index 000000000..6dcfdc36f --- /dev/null +++ b/web/components/gantt-chart/chart/header.tsx @@ -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) => { + const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props; + // chart hook + const { currentView, allViews } = useChart(); + + return ( +
+
{title}
+
+
{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}
+
+ +
+ {allViews?.map((chartView: any) => ( +
handleChartView(chartView?.key)} + > + {chartView?.title} +
+ ))} +
+ + + + +
+ ); +}; diff --git a/web/components/gantt-chart/chart/index.ts b/web/components/gantt-chart/chart/index.ts new file mode 100644 index 000000000..68b20b89a --- /dev/null +++ b/web/components/gantt-chart/chart/index.ts @@ -0,0 +1,4 @@ +export * from "./views"; +export * from "./header"; +export * from "./main-content"; +export * from "./root"; diff --git a/web/components/gantt-chart/chart/index.tsx b/web/components/gantt-chart/chart/index.tsx deleted file mode 100644 index 4592bfb5b..000000000 --- a/web/components/gantt-chart/chart/index.tsx +++ /dev/null @@ -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 = (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(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 ( -
- {/* chart header */} -
- {title && ( -
-
{title}
- {/*
- Gantt View Beta -
*/} -
- )} - -
- {blocks === null ? ( -
Loading...
- ) : ( -
- {blocks.length} {loaderTitle} -
- )} -
- -
- {allViews && - allViews.length > 0 && - // eslint-disable-next-line @typescript-eslint/no-unused-vars - allViews.map((_chatView: any, _idx: any) => ( -
handleChartView(_chatView?.key)} - > - {_chatView?.title} -
- ))} -
- -
-
- Today -
-
- -
setFullScreenMode((prevData) => !prevData)} - > - {fullScreenMode ? : } -
-
- - {/* content */} -
-
-
-
{title}
-
Duration
-
- - {sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })} -
-
- {/* {currentView && currentView === "hours" && } */} - {/* {currentView && currentView === "day" && } */} - {/* {currentView && currentView === "week" && } */} - {/* {currentView && currentView === "bi_week" && } */} - {currentView && currentView === "month" && } - {/* {currentView && currentView === "quarter" && } */} - {/* {currentView && currentView === "year" && } */} - - {/* blocks */} - {currentView && currentViewData && ( - - )} -
-
-
- ); -}; diff --git a/web/components/gantt-chart/chart/main-content.tsx b/web/components/gantt-chart/chart/main-content.tsx new file mode 100644 index 000000000..950f3550c --- /dev/null +++ b/web/components/gantt-chart/chart/main-content.tsx @@ -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) => { + 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) => { + 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 ( +
+ +
+ + {currentViewData && ( + + )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/chart/month.tsx b/web/components/gantt-chart/chart/month.tsx deleted file mode 100644 index 0b7a4c452..000000000 --- a/web/components/gantt-chart/chart/month.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { FC } from "react"; -// hooks -import { useChart } from "../hooks"; -// types -import { IMonthBlock } from "../views"; - -export const MonthChartView: FC = () => { - const { currentViewData, renderView } = useChart(); - - const monthBlocks: IMonthBlock[] = renderView; - - return ( - <> -
- {monthBlocks && - monthBlocks.length > 0 && - monthBlocks.map((block, _idxRoot) => ( -
-
-
-
- {block?.title} -
-
- -
- {block?.children && - block?.children.length > 0 && - block?.children.map((monthDay, _idx) => ( -
-
- {monthDay.dayData.shortTitle[0]}{" "} - - {monthDay.day} - -
-
- ))} -
-
- -
- {block?.children && - block?.children.length > 0 && - block?.children.map((monthDay, _idx) => ( -
-
- {/* {monthDay?.today && ( -
- )} */} -
-
- ))} -
-
- ))} -
- - ); -}; diff --git a/web/components/gantt-chart/chart/root.tsx b/web/components/gantt-chart/chart/root.tsx new file mode 100644 index 000000000..961cb2fee --- /dev/null +++ b/web/components/gantt-chart/chart/root.tsx @@ -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 = (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(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 ( +
+ setFullScreenMode((prevData) => !prevData)} + handleChartView={(key) => updateCurrentViewRenderPayload(null, key)} + handleToday={handleToday} + loaderTitle={loaderTitle} + title={title} + /> + +
+ ); +}; diff --git a/web/components/gantt-chart/chart/bi-week.tsx b/web/components/gantt-chart/chart/views/bi-week.tsx similarity index 97% rename from web/components/gantt-chart/chart/bi-week.tsx rename to web/components/gantt-chart/chart/views/bi-week.tsx index f4a6080cd..6e53d5390 100644 --- a/web/components/gantt-chart/chart/bi-week.tsx +++ b/web/components/gantt-chart/chart/views/bi-week.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "components/gantt-chart"; export const BiWeekChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/chart/day.tsx b/web/components/gantt-chart/chart/views/day.tsx similarity index 98% rename from web/components/gantt-chart/chart/day.tsx rename to web/components/gantt-chart/chart/views/day.tsx index 32b3caca0..a50b7748a 100644 --- a/web/components/gantt-chart/chart/day.tsx +++ b/web/components/gantt-chart/chart/views/day.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "../../hooks"; export const DayChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/chart/hours.tsx b/web/components/gantt-chart/chart/views/hours.tsx similarity index 97% rename from web/components/gantt-chart/chart/hours.tsx rename to web/components/gantt-chart/chart/views/hours.tsx index 5693b38b8..e1fd02e3f 100644 --- a/web/components/gantt-chart/chart/hours.tsx +++ b/web/components/gantt-chart/chart/views/hours.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "components/gantt-chart"; export const HourChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/chart/views/index.ts b/web/components/gantt-chart/chart/views/index.ts new file mode 100644 index 000000000..8936623c2 --- /dev/null +++ b/web/components/gantt-chart/chart/views/index.ts @@ -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"; diff --git a/web/components/gantt-chart/chart/views/month.tsx b/web/components/gantt-chart/chart/views/month.tsx new file mode 100644 index 000000000..13f60c2b4 --- /dev/null +++ b/web/components/gantt-chart/chart/views/month.tsx @@ -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 = () => { + // chart hook + const { currentViewData, renderView } = useChart(); + const monthBlocks: IMonthBlock[] = renderView; + + return ( + <> +
+ {monthBlocks?.map((block, rootIndex) => ( +
+
+
+
+ {block?.title} +
+
+
+ {block?.children?.map((monthDay, index) => ( +
+
+ {monthDay.dayData.shortTitle[0]}{" "} + + {monthDay.day} + +
+
+ ))} +
+
+
+ {block?.children?.map((monthDay, index) => ( +
+ {["sat", "sun"].includes(monthDay?.dayData?.shortTitle) && ( +
+ )} +
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/web/components/gantt-chart/chart/quarter.tsx b/web/components/gantt-chart/chart/views/quarter.tsx similarity index 98% rename from web/components/gantt-chart/chart/quarter.tsx rename to web/components/gantt-chart/chart/views/quarter.tsx index a15f6f34d..ffbc1cbfe 100644 --- a/web/components/gantt-chart/chart/quarter.tsx +++ b/web/components/gantt-chart/chart/views/quarter.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "../../hooks"; export const QuarterChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/chart/week.tsx b/web/components/gantt-chart/chart/views/week.tsx similarity index 98% rename from web/components/gantt-chart/chart/week.tsx rename to web/components/gantt-chart/chart/views/week.tsx index b90caf8b7..8170affa4 100644 --- a/web/components/gantt-chart/chart/week.tsx +++ b/web/components/gantt-chart/chart/views/week.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "../../hooks"; export const WeekChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/chart/year.tsx b/web/components/gantt-chart/chart/views/year.tsx similarity index 98% rename from web/components/gantt-chart/chart/year.tsx rename to web/components/gantt-chart/chart/views/year.tsx index 7c3a34b53..9dbeedece 100644 --- a/web/components/gantt-chart/chart/year.tsx +++ b/web/components/gantt-chart/chart/views/year.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "../../hooks"; export const YearChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/constants.ts b/web/components/gantt-chart/constants.ts new file mode 100644 index 000000000..958985cf1 --- /dev/null +++ b/web/components/gantt-chart/constants.ts @@ -0,0 +1,5 @@ +export const BLOCK_HEIGHT = 44; + +export const HEADER_HEIGHT = 60; + +export const SIDEBAR_WIDTH = 360; diff --git a/web/components/gantt-chart/contexts/index.tsx b/web/components/gantt-chart/contexts/index.tsx index 137cc2607..84e7a19b5 100644 --- a/web/components/gantt-chart/contexts/index.tsx +++ b/web/components/gantt-chart/contexts/index.tsx @@ -24,6 +24,7 @@ const chartReducer = (state: ChartContextData, action: ChartContextActionPayload const initialView = "month"; export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + // states; const [state, dispatch] = useState({ currentView: initialView, currentViewData: currentViewDataWithView(initialView), @@ -31,23 +32,25 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ allViews: allViewsWithData, activeBlock: null, }); - const [scrollLeft, setScrollLeft] = useState(0); const handleDispatch = (action: ChartContextActionPayload): ChartContextData => { const newState = chartReducer(state, action); - dispatch(() => newState); - return newState; }; - const updateScrollLeft = (scrollLeft: number) => { - setScrollLeft(scrollLeft); - }; + const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft); return ( - + {children} ); diff --git a/web/components/gantt-chart/helpers/block-structure.tsx b/web/components/gantt-chart/helpers/block-structure.ts similarity index 100% rename from web/components/gantt-chart/helpers/block-structure.tsx rename to web/components/gantt-chart/helpers/block-structure.ts diff --git a/web/components/gantt-chart/helpers/draggable.tsx b/web/components/gantt-chart/helpers/draggable.tsx index d2c4448bb..ac1602346 100644 --- a/web/components/gantt-chart/helpers/draggable.tsx +++ b/web/components/gantt-chart/helpers/draggable.tsx @@ -1,9 +1,11 @@ import React, { useEffect, useRef, useState } from "react"; -import { ArrowLeft, ArrowRight } from "lucide-react"; +import { ArrowRight } from "lucide-react"; // hooks -import { useChart } from "../hooks"; -// types -import { IGanttBlock } from "../types"; +import { IGanttBlock, useChart } from "components/gantt-chart"; +// helpers +import { cn } from "helpers/common.helper"; +// constants +import { SIDEBAR_WIDTH } from "../constants"; type Props = { block: IGanttBlock; @@ -20,7 +22,7 @@ export const ChartDraggable: React.FC = (props) => { const [isLeftResizing, setIsLeftResizing] = useState(false); const [isRightResizing, setIsRightResizing] = useState(false); const [isMoving, setIsMoving] = useState(false); - const [posFromLeft, setPosFromLeft] = useState(null); + const [isHidden, setIsHidden] = useState(true); // refs const resizableRef = useRef(null); // chart hook @@ -31,12 +33,10 @@ export const ChartDraggable: React.FC = (props) => { let delWidth = 0; - const ganttContainer = document.querySelector("#gantt-container") as HTMLElement; - const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLElement; + const ganttContainer = document.querySelector("#gantt-container") as HTMLDivElement; + const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLDivElement; - const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; - - if (!ganttContainer || !ganttSidebar || !scrollContainer) return 0; + if (!ganttContainer || !ganttSidebar) return 0; const posFromLeft = e.clientX; // manually scroll to left if reached the left end while dragging @@ -45,7 +45,7 @@ export const ChartDraggable: React.FC = (props) => { delWidth = -5; - scrollContainer.scrollBy(delWidth, 0); + ganttContainer.scrollBy(delWidth, 0); } else delWidth = e.movementX; // manually scroll to right if reached the right end while dragging @@ -55,7 +55,7 @@ export const ChartDraggable: React.FC = (props) => { delWidth = 5; - scrollContainer.scrollBy(delWidth, 0); + ganttContainer.scrollBy(delWidth, 0); } else delWidth = e.movementX; return delWidth; @@ -201,50 +201,61 @@ export const ChartDraggable: React.FC = (props) => { }; // scroll to a hidden block const handleScrollToBlock = () => { - const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; - + const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement; if (!scrollContainer || !block.position) return; - // update container's scroll position to the block's position 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 const isBlockHiddenOnLeft = 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 ( <> - {/* move to left side hidden block button */} - {isBlockHiddenOnLeft && ( - - )} - {/* move to right side hidden block button */} - {isBlockHiddenOnRight && ( -
- -
+ + )}
= (props) => { onMouseDown={handleBlockLeftResize} onMouseEnter={() => setIsLeftResizing(true)} 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" />
)}
{blockToRender(block.data)} @@ -281,12 +297,15 @@ export const ChartDraggable: React.FC = (props) => { onMouseDown={handleBlockRightResize} onMouseEnter={() => setIsRightResizing(true)} 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" />
)} diff --git a/web/components/gantt-chart/index.ts b/web/components/gantt-chart/index.ts index ead696086..54a2cc597 100644 --- a/web/components/gantt-chart/index.ts +++ b/web/components/gantt-chart/index.ts @@ -1,4 +1,5 @@ export * from "./blocks"; +export * from "./chart"; export * from "./helpers"; export * from "./hooks"; export * from "./root"; diff --git a/web/components/gantt-chart/root.tsx b/web/components/gantt-chart/root.tsx index 7673da88e..c8576436e 100644 --- a/web/components/gantt-chart/root.tsx +++ b/web/components/gantt-chart/root.tsx @@ -1,10 +1,8 @@ import { FC } from "react"; // components -import { ChartViewRoot } from "./chart"; +import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; // context import { ChartContextProvider } from "./contexts"; -// types -import { IBlockUpdateData, IGanttBlock } from "./types"; type GanttChartRootProps = { border?: boolean; diff --git a/web/components/gantt-chart/sidebar/cycle-sidebar.tsx b/web/components/gantt-chart/sidebar/cycles.tsx similarity index 85% rename from web/components/gantt-chart/sidebar/cycle-sidebar.tsx rename to web/components/gantt-chart/sidebar/cycles.tsx index dddccda5a..384869a40 100644 --- a/web/components/gantt-chart/sidebar/cycle-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/cycles.tsx @@ -1,4 +1,3 @@ -import { useRouter } from "next/router"; import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; import { MoreVertical } from "lucide-react"; // hooks @@ -9,8 +8,11 @@ import { Loader } from "@plane/ui"; import { CycleGanttSidebarBlock } from "components/cycles"; // helpers import { findTotalDaysInRange } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; // types import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; +// constants +import { BLOCK_HEIGHT } from "../constants"; type Props = { title: string; @@ -20,12 +22,8 @@ type Props = { }; export const CycleGanttSidebar: React.FC = (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, blockUpdateHandler, blocks, enableReorder } = props; - - const router = useRouter(); - const { cycleId } = router.query; - + const { blockUpdateHandler, blocks, enableReorder } = props; + // chart hook const { activeBlock, dispatch } = useChart(); // update the active block on hover @@ -84,12 +82,7 @@ export const CycleGanttSidebar: React.FC = (props) => { {(droppableProvided) => ( -
+
<> {blocks ? ( blocks.map((block, index) => { @@ -104,7 +97,9 @@ export const CycleGanttSidebar: React.FC = (props) => { > {(provided, snapshot) => (
updateActiveBlock(block)} onMouseLeave={() => updateActiveBlock(null)} ref={provided.innerRef} @@ -112,9 +107,12 @@ export const CycleGanttSidebar: React.FC = (props) => { >
{enableReorder && ( + )} +
+
+ +
+ {duration && ( +
+ + {duration} day{duration > 1 ? "s" : ""} + +
+ )} +
+
+
+ )} + + ); + }) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} + + + {enableQuickIssueCreate && !disableIssueCreation && ( + + )} + + ); +}); diff --git a/web/components/gantt-chart/sidebar/module-sidebar.tsx b/web/components/gantt-chart/sidebar/modules.tsx similarity index 86% rename from web/components/gantt-chart/sidebar/module-sidebar.tsx rename to web/components/gantt-chart/sidebar/modules.tsx index 8f8788787..bdf8ca571 100644 --- a/web/components/gantt-chart/sidebar/module-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/modules.tsx @@ -1,4 +1,3 @@ -import { useRouter } from "next/router"; import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; import { MoreVertical } from "lucide-react"; // hooks @@ -9,8 +8,11 @@ import { Loader } from "@plane/ui"; import { ModuleGanttSidebarBlock } from "components/modules"; // helpers import { findTotalDaysInRange } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; // types import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; +// constants +import { BLOCK_HEIGHT } from "../constants"; type Props = { title: string; @@ -20,12 +22,8 @@ type Props = { }; export const ModuleGanttSidebar: React.FC = (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, blockUpdateHandler, blocks, enableReorder } = props; - - const router = useRouter(); - const { cycleId } = router.query; - + const { blockUpdateHandler, blocks, enableReorder } = props; + // chart hook const { activeBlock, dispatch } = useChart(); // update the active block on hover @@ -84,12 +82,7 @@ export const ModuleGanttSidebar: React.FC = (props) => { {(droppableProvided) => ( -
+
<> {blocks ? ( blocks.map((block, index) => { @@ -104,7 +97,9 @@ export const ModuleGanttSidebar: React.FC = (props) => { > {(provided, snapshot) => (
updateActiveBlock(block)} onMouseLeave={() => updateActiveBlock(null)} ref={provided.innerRef} @@ -112,9 +107,12 @@ export const ModuleGanttSidebar: React.FC = (props) => { >
{enableReorder && ( - )} -
-
- -
- {duration !== undefined && ( -
- - {duration} day{duration > 1 ? "s" : ""} - -
- )} -
-
-
- )} - - ); - }) - ) : ( - - - - - - - )} - {droppableProvided.placeholder} - - {enableQuickIssueCreate && !disableIssueCreation && ( - - )} -
- )} - - - ); -}; diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 601205b5c..b0eb2be9c 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -69,7 +69,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan loaderTitle="Issues" blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null} blockUpdateHandler={updateIssueBlockStructure} - blockToRender={(data: TIssue) => } + blockToRender={(data: TIssue) => } sidebarToRender={(props) => ( { - // hooks +type Props = { + issueId: string; +}; + +export const IssueGanttBlock: React.FC = observer((props) => { + const { issueId } = props; + // store hooks const { router: { workspaceSlug }, } = useApplication(); 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 = () => workspaceSlug && - data && - data.project_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 || ""; + issueDetails && + setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id }); return (
@@ -35,58 +43,62 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => { -
{data?.name}
+
{issueDetails?.name}
- {renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.target_date ?? "")} + {renderFormattedDate(issueDetails?.start_date ?? "")} to{" "} + {renderFormattedDate(issueDetails?.target_date ?? "")}
} position="top-left" > -
{data?.name}
+
+ {issueDetails?.name} +
); -}; +}); // rendering issues on gantt sidebar -export const IssueGanttSidebarBlock = ({ data }: { data: TIssue }) => { - // hooks - const { getProjectStates } = useProjectState(); +export const IssueGanttSidebarBlock: React.FC = observer((props) => { + const { issueId } = props; + // store hooks + const { getStateById } = useProjectState(); const { getProjectById } = useProject(); const { router: { workspaceSlug }, } = 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 = () => workspaceSlug && - data && - data.project_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; + issueDetails && + setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id }); return (
- {currentStateDetails != undefined && ( - - )} + {stateDetails && }
- {getProjectById(data?.project_id)?.identifier} {data?.sequence_id} + {projectDetails?.identifier} {issueDetails?.sequence_id}
- - {data?.name} + + {issueDetails?.name}
); -}; +}); diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index bfecb993b..1ddd21ce2 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -11,6 +11,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { createIssuePayload } from "helpers/issue.helper"; +import { cn } from "helpers/common.helper"; // types import { IProject, TIssue } from "@plane/types"; // constants @@ -138,10 +139,12 @@ export const GanttQuickAddIssueForm: React.FC = observe }; return ( <> -
- {isOpen ? ( + {isOpen ? ( +
= observe
{`Press 'Enter' to add another issue`}
- ) : ( -
setIsOpen(true)} - > - - New Issue -
- )} -
+
+ ) : ( + + )} ); }); diff --git a/web/components/modules/gantt-chart/blocks.tsx b/web/components/modules/gantt-chart/blocks.tsx index 72717f12b..188d7b130 100644 --- a/web/components/modules/gantt-chart/blocks.tsx +++ b/web/components/modules/gantt-chart/blocks.tsx @@ -1,52 +1,74 @@ import { useRouter } from "next/router"; +import { observer } from "mobx-react"; +// hooks +import { useApplication, useModule } from "hooks/store"; // ui import { Tooltip, ModuleStatusIcon } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; -// types -import { IModule } from "@plane/types"; // constants import { MODULE_STATUS } from "constants/module"; -export const ModuleGanttBlock = ({ data }: { data: IModule }) => { +type Props = { + moduleId: string; +}; + +export const ModuleGanttBlock: React.FC = observer((props) => { + const { moduleId } = props; + // router const router = useRouter(); - const { workspaceSlug } = router.query; + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getModuleById } = useModule(); + // derived values + const moduleDetails = getModuleById(moduleId); return (
s.value === data?.status)?.color }} - onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data?.id}`)} + style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color }} + onClick={() => router.push(`/${workspaceSlug}/projects/${moduleDetails?.project}/modules/${moduleDetails?.id}`)} >
-
{data?.name}
+
{moduleDetails?.name}
- {renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.target_date ?? "")} + {renderFormattedDate(moduleDetails?.start_date ?? "")} to{" "} + {renderFormattedDate(moduleDetails?.target_date ?? "")}
} position="top-left" > -
{data?.name}
+
{moduleDetails?.name}
); -}; +}); -export const ModuleGanttSidebarBlock = ({ data }: { data: IModule }) => { +export const ModuleGanttSidebarBlock: React.FC = observer((props) => { + const { moduleId } = props; + // router const router = useRouter(); - const { workspaceSlug } = router.query; + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getModuleById } = useModule(); + // derived values + const moduleDetails = getModuleById(moduleId); return (
router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data.id}`)} + onClick={() => router.push(`/${workspaceSlug}/projects/${moduleDetails?.project}/modules/${moduleDetails?.id}`)} > - -
{data.name}
+ +
{moduleDetails?.name}
); -}; +}); diff --git a/web/components/modules/gantt-chart/modules-list-layout.tsx b/web/components/modules/gantt-chart/modules-list-layout.tsx index 53948f71d..a3f37df5c 100644 --- a/web/components/modules/gantt-chart/modules-list-layout.tsx +++ b/web/components/modules/gantt-chart/modules-list-layout.tsx @@ -47,7 +47,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => { blocks={projectModuleIds ? blockFormat(projectModuleIds) : null} sidebarToRender={(props) => } blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)} - blockToRender={(data: IModule) => } + blockToRender={(data: IModule) => } enableBlockLeftResize={isAllowed} enableBlockRightResize={isAllowed} enableBlockMove={isAllowed}