From ce43067bc11291a02677e981384ab7e67694955c Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Mon, 18 Mar 2024 13:12:10 +0530 Subject: [PATCH] restructure gantt layout charts --- .../cycles/gantt-chart/cycles-list-layout.tsx | 46 ++++++----- web/components/gantt-chart/blocks/block.tsx | 22 ++++-- .../gantt-chart/blocks/blocks-list.tsx | 19 ++--- web/components/gantt-chart/chart/header.tsx | 8 +- .../gantt-chart/chart/main-content.tsx | 18 +++-- web/components/gantt-chart/chart/root.tsx | 36 +++------ web/components/gantt-chart/root.tsx | 14 +++- .../gantt-chart/sidebar/cycles/sidebar.tsx | 68 +++++++++-------- .../sidebar/issues/issue-draggable-block.tsx | 34 +++++++++ .../gantt-chart/sidebar/issues/sidebar.tsx | 76 +++++++++---------- .../gantt-chart/sidebar/modules/sidebar.tsx | 67 ++++++++-------- web/components/gantt-chart/sidebar/root.tsx | 20 ++++- web/components/gantt-chart/types/index.ts | 4 +- .../gantt-chart/views/month-view.ts | 2 +- .../issue-layouts/gantt/base-gantt-root.tsx | 45 ++++++++--- .../gantt-chart/modules-list-layout.tsx | 41 ++++++---- web/helpers/issue.helper.ts | 11 +-- 17 files changed, 322 insertions(+), 209 deletions(-) create mode 100644 web/components/gantt-chart/sidebar/issues/issue-draggable-block.tsx diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 094fbea7b..283d0298a 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -1,13 +1,14 @@ -import { FC } from "react"; +import { FC, useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { CycleGanttBlock } from "components/cycles"; -import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; +import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar, ChartDataType } from "components/gantt-chart"; import { useCycle } from "hooks/store"; // components // types import { ICycle } from "@plane/types"; +import { getMonthChartItemPositionWidthInMonth } from "components/gantt-chart/views"; // constants type Props = { @@ -23,6 +24,28 @@ export const CyclesListGanttChartView: FC = observer((props) => { // store hooks const { getCycleById, updateCycleDetails } = useCycle(); + const getBlockById = useCallback( + (id: string, currentViewData?: ChartDataType | undefined) => { + const cycle = getCycleById(id); + const block = { + data: cycle, + id: cycle?.id ?? "", + sort_order: cycle?.sort_order ?? 0, + start_date: cycle?.start_date ? new Date(cycle?.start_date) : undefined, + target_date: cycle?.end_date ? new Date(cycle?.end_date) : undefined, + }; + + if (currentViewData) { + return { + ...block, + position: getMonthChartItemPositionWidthInMonth(currentViewData, block), + }; + } + return block; + }, + [getCycleById] + ); + const handleCycleUpdate = async (cycle: ICycle, data: IBlockUpdateData) => { if (!workspaceSlug || !cycle) return; @@ -32,28 +55,13 @@ export const CyclesListGanttChartView: FC = observer((props) => { await updateCycleDetails(workspaceSlug.toString(), cycle.project_id, cycle.id, payload); }; - const blockFormat = (blocks: (ICycle | null)[]) => { - if (!blocks) return []; - - const filteredBlocks = blocks.filter((b) => b !== null && b.start_date && b.end_date); - - const structuredBlocks = filteredBlocks.map((block) => ({ - data: block, - id: block?.id ?? "", - sort_order: block?.sort_order ?? 0, - start_date: new Date(block?.start_date ?? ""), - target_date: new Date(block?.end_date ?? ""), - })); - - return structuredBlocks; - }; - return (
getCycleById(c))) : null} + blockIds={cycleIds} + getBlockById={getBlockById} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} sidebarToRender={(props) => } blockToRender={(data: ICycle) => } diff --git a/web/components/gantt-chart/blocks/block.tsx b/web/components/gantt-chart/blocks/block.tsx index 3305c9846..59f55a053 100644 --- a/web/components/gantt-chart/blocks/block.tsx +++ b/web/components/gantt-chart/blocks/block.tsx @@ -10,10 +10,12 @@ import { useIssueDetail } from "hooks/store"; import { BLOCK_HEIGHT } from "../constants"; import { ChartAddBlock, ChartDraggable } from "../helpers"; import { useGanttChart } from "../hooks"; -import { IBlockUpdateData, IGanttBlock } from "../types"; +import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types"; type Props = { - block: IGanttBlock; + blockId: string; + getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; + showAllBlocks: boolean; blockToRender: (data: any) => React.ReactNode; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; enableBlockLeftResize: boolean; @@ -25,7 +27,9 @@ type Props = { export const GanttChartBlock: React.FC = observer((props) => { const { - block, + blockId, + getBlockById, + showAllBlocks, blockToRender, blockUpdateHandler, enableBlockLeftResize, @@ -35,9 +39,14 @@ export const GanttChartBlock: React.FC = observer((props) => { ganttContainerRef, } = props; // store hooks - const { updateActiveBlockId, isBlockActive } = useGanttChart(); + const { currentViewData, updateActiveBlockId, isBlockActive } = useGanttChart(); const { peekIssue } = useIssueDetail(); + const block = getBlockById(blockId, currentViewData); + + // hide the block if it doesn't have start and target dates and showAllBlocks is false + if (!block || (!showAllBlocks && !(block.start_date && block.target_date))) return null; + const isBlockVisibleOnChart = block.start_date && block.target_date; const handleChartBlockPosition = ( @@ -72,7 +81,6 @@ export const GanttChartBlock: React.FC = observer((props) => { return (
= observer((props) => { >
updateActiveBlockId(block.id)} + onMouseEnter={() => updateActiveBlockId(blockId)} onMouseLeave={() => updateActiveBlockId(null)} > {isBlockVisibleOnChart ? ( diff --git a/web/components/gantt-chart/blocks/blocks-list.tsx b/web/components/gantt-chart/blocks/blocks-list.tsx index 8eb1d8772..98bf7be1c 100644 --- a/web/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/components/gantt-chart/blocks/blocks-list.tsx @@ -1,14 +1,15 @@ import { FC } from "react"; // components import { HEADER_HEIGHT } from "../constants"; -import { IBlockUpdateData, IGanttBlock } from "../types"; +import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types"; import { GanttChartBlock } from "./block"; // types // constants export type GanttChartBlocksProps = { itemsContainerWidth: number; - blocks: IGanttBlock[] | null; + blockIds: string[]; + getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; blockToRender: (data: any) => React.ReactNode; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; enableBlockLeftResize: boolean; @@ -22,9 +23,10 @@ export type GanttChartBlocksProps = { export const GanttChartBlocksList: FC = (props) => { const { itemsContainerWidth, - blocks, + blockIds, blockToRender, blockUpdateHandler, + getBlockById, enableBlockLeftResize, enableBlockRightResize, enableBlockMove, @@ -41,14 +43,13 @@ export const GanttChartBlocksList: FC = (props) => { transform: `translateY(${HEADER_HEIGHT}px)`, }} > - {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; - + {blockIds?.map((blockId) => { return ( void; handleToday: () => void; @@ -20,7 +20,7 @@ type Props = { }; export const GanttChartHeader: React.FC = observer((props) => { - const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props; + const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props; // chart hook const { currentView } = useGanttChart(); @@ -28,7 +28,9 @@ export const GanttChartHeader: React.FC = observer((props) => {
{title}
-
{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}
+
+ {blockIds ? `${blockIds.length} ${loaderTitle}` : "Loading..."} +
diff --git a/web/components/gantt-chart/chart/main-content.tsx b/web/components/gantt-chart/chart/main-content.tsx index 8c00fde1b..914ffbb47 100644 --- a/web/components/gantt-chart/chart/main-content.tsx +++ b/web/components/gantt-chart/chart/main-content.tsx @@ -4,6 +4,7 @@ import { observer } from "mobx-react"; // components import { BiWeekChartView, + ChartDataType, DayChartView, GanttChartBlocksList, GanttChartSidebar, @@ -21,11 +22,12 @@ import { cn } from "helpers/common.helper"; import { useGanttChart } from "../hooks/use-gantt-chart"; type Props = { - blocks: IGanttBlock[] | null; + blockIds: string[]; + getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; + loadMoreBlocks?: () => void; blockToRender: (data: any) => React.ReactNode; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; bottomSpacing: boolean; - chartBlocks: IGanttBlock[] | null; enableBlockLeftResize: boolean; enableBlockMove: boolean; enableBlockRightResize: boolean; @@ -41,11 +43,12 @@ type Props = { export const GanttChartMainContent: React.FC = observer((props) => { const { - blocks, + blockIds, + getBlockById, + loadMoreBlocks, blockToRender, blockUpdateHandler, bottomSpacing, - chartBlocks, enableBlockLeftResize, enableBlockMove, enableBlockRightResize, @@ -104,7 +107,9 @@ export const GanttChartMainContent: React.FC = observer((props) => { onScroll={onScroll} > = observer((props) => { {currentViewData && ( void; blockToRender: (data: any) => React.ReactNode; sidebarToRender: (props: any) => React.ReactNode; @@ -34,6 +30,8 @@ type ChartViewRootProps = { enableAddBlock: boolean; bottomSpacing: boolean; showAllBlocks: boolean; + getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; + loadMoreBlocks?: () => void; quickAdd?: React.JSX.Element | undefined; }; @@ -41,7 +39,9 @@ export const ChartViewRoot: FC = observer((props) => { const { border, title, - blocks = null, + blockIds, + getBlockById, + loadMoreBlocks, loaderTitle, blockUpdateHandler, sidebarToRender, @@ -58,25 +58,10 @@ export const ChartViewRoot: FC = observer((props) => { // states const [itemsContainerWidth, setItemsContainerWidth] = useState(0); const [fullScreenMode, setFullScreenMode] = useState(false); - const [chartBlocks, setChartBlocks] = useState(null); // hooks const { currentView, currentViewData, renderView, updateCurrentView, updateCurrentViewData, updateRenderView } = useGanttChart(); - // 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 = @@ -166,7 +151,7 @@ export const ChartViewRoot: FC = observer((props) => { })} > setFullScreenMode((prevData) => !prevData)} handleChartView={(key) => updateCurrentViewRenderPayload(null, key)} @@ -175,11 +160,12 @@ export const ChartViewRoot: FC = observer((props) => { title={title} /> void; blockToRender: (data: any) => React.ReactNode; sidebarToRender: (props: any) => React.ReactNode; quickAdd?: React.JSX.Element | undefined; + getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; + loadMoreBlocks?: () => void; enableBlockLeftResize?: boolean; enableBlockRightResize?: boolean; enableBlockMove?: boolean; @@ -26,11 +28,13 @@ export const GanttChartRoot: FC = (props) => { const { border = true, title, - blocks, + blockIds, loaderTitle = "blocks", blockUpdateHandler, sidebarToRender, blockToRender, + getBlockById, + loadMoreBlocks, enableBlockLeftResize = false, enableBlockRightResize = false, enableBlockMove = false, @@ -46,7 +50,9 @@ export const GanttChartRoot: FC = (props) => { void; - blocks: IGanttBlock[] | null; + getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; + blockIds: string[]; enableReorder: boolean; }; export const CycleGanttSidebar: React.FC = (props) => { - const { blockUpdateHandler, blocks, enableReorder } = props; + const { blockUpdateHandler, blockIds, getBlockById, enableReorder } = props; const handleOrderChange = (result: DropResult) => { - if (!blocks) return; + if (!blockIds) return; const { source, destination } = result; @@ -27,29 +28,30 @@ export const CycleGanttSidebar: React.FC = (props) => { // return if dropped on the same index if (source.index === destination.index) return; - let updatedSortOrder = blocks[source.index].sort_order; + let updatedSortOrder = getBlockById(blockIds[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; + if (destination.index === 0) updatedSortOrder = getBlockById(blockIds[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; + else if (destination.index === blockIds.length - 1) + updatedSortOrder = getBlockById(blockIds[blockIds.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 destinationSortingOrder = getBlockById(blockIds[destination.index]).sort_order; const relativeDestinationSortingOrder = source.index < destination.index - ? blocks[destination.index + 1].sort_order - : blocks[destination.index - 1].sort_order; + ? getBlockById(blockIds[destination.index + 1]).sort_order + : getBlockById(blockIds[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); + const removedElement = blockIds.splice(source.index, 1)[0]; + blockIds.splice(destination.index, 0, removedElement); // call the block update handler with the updated sort order, new and old index - blockUpdateHandler(removedElement.data, { + blockUpdateHandler(getBlockById(removedElement).data, { sort_order: { destinationIndex: destination.index, newSortOrder: updatedSortOrder, @@ -64,24 +66,28 @@ export const CycleGanttSidebar: React.FC = (props) => { {(droppableProvided) => (
<> - {blocks ? ( - blocks.map((block, index) => ( - - {(provided, snapshot) => ( - - )} - - )) + {blockIds ? ( + blockIds.map((blockId, index) => { + const block = getBlockById(blockId); + if (!block.start_date || !block.target_date) return null; + return ( + + {(provided, snapshot) => ( + + )} + + ); + }) ) : ( diff --git a/web/components/gantt-chart/sidebar/issues/issue-draggable-block.tsx b/web/components/gantt-chart/sidebar/issues/issue-draggable-block.tsx new file mode 100644 index 000000000..296cd8480 --- /dev/null +++ b/web/components/gantt-chart/sidebar/issues/issue-draggable-block.tsx @@ -0,0 +1,34 @@ +import { Draggable } from "@hello-pangea/dnd"; +import { observer } from "mobx-react"; +import { IssuesSidebarBlock } from "./block"; +import { IGanttBlock } from "components/gantt-chart/types"; + +interface Props { + blockId: string; + enableReorder: boolean; + index: number; + showAllBlocks: boolean; + getBlockById: (blockId: string) => IGanttBlock; +} +export const IssueDraggableBlock = observer((props: Props) => { + const { blockId, enableReorder, index, showAllBlocks, getBlockById } = props; + const block = getBlockById(blockId); + + 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 null; + + return ( + + {(provided, snapshot) => ( + + )} + + ); +}); diff --git a/web/components/gantt-chart/sidebar/issues/sidebar.tsx b/web/components/gantt-chart/sidebar/issues/sidebar.tsx index e82e40f5d..3c808ba34 100644 --- a/web/components/gantt-chart/sidebar/issues/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -4,20 +4,29 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea import { Loader } from "@plane/ui"; // types import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; -import { IssuesSidebarBlock } from "./block"; +import { observer } from "mobx-react"; +import { IssueDraggableBlock } from "./issue-draggable-block"; +import { useIntersectionObserver } from "hooks/use-intersection-observer"; +import { useRef } from "react"; type Props = { blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - blocks: IGanttBlock[] | null; + getBlockById: (id: string) => IGanttBlock; + loadMoreBlocks?: () => void; + blockIds: string[]; enableReorder: boolean; showAllBlocks?: boolean; }; -export const IssueGanttSidebar: React.FC = (props) => { - const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props; +export const IssueGanttSidebar: React.FC = observer((props) => { + const { blockUpdateHandler, blockIds, getBlockById, enableReorder, loadMoreBlocks, showAllBlocks = false } = props; + + const intersectionRef = useRef(null); + + useIntersectionObserver(undefined, intersectionRef, loadMoreBlocks); const handleOrderChange = (result: DropResult) => { - if (!blocks) return; + if (!blockIds) return; const { source, destination } = result; @@ -27,29 +36,30 @@ export const IssueGanttSidebar: React.FC = (props) => { // return if dropped on the same index if (source.index === destination.index) return; - let updatedSortOrder = blocks[source.index].sort_order; + let updatedSortOrder = getBlockById(blockIds[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; + if (destination.index === 0) updatedSortOrder = getBlockById(blockIds[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; + else if (destination.index === blockIds.length - 1) + updatedSortOrder = getBlockById(blockIds[blockIds.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 destinationSortingOrder = getBlockById(blockIds[destination.index]).sort_order; const relativeDestinationSortingOrder = source.index < destination.index - ? blocks[destination.index + 1].sort_order - : blocks[destination.index - 1].sort_order; + ? getBlockById(blockIds[destination.index + 1]).sort_order + : getBlockById(blockIds[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); + const removedElement = blockIds.splice(source.index, 1)[0]; + blockIds.splice(destination.index, 0, removedElement); // call the block update handler with the updated sort order, new and old index - blockUpdateHandler(removedElement.data, { + blockUpdateHandler(getBlockById(removedElement).data, { sort_order: { destinationIndex: destination.index, newSortOrder: updatedSortOrder, @@ -64,31 +74,19 @@ export const IssueGanttSidebar: React.FC = (props) => { {(droppableProvided) => (
<> - {blocks ? ( - blocks.map((block, index) => { - const isBlockVisibleOnSidebar = block.start_date && block.target_date; - - // hide the block if it doesn't have start and target dates and showAllBlocks is false - if (!showAllBlocks && !isBlockVisibleOnSidebar) return; - - return ( - + {blockIds.map((blockId, index) => ( + - {(provided, snapshot) => ( - - )} - - ); - }) + showAllBlocks={showAllBlocks} + getBlockById={getBlockById} + /> + ))} + + ) : ( @@ -104,4 +102,4 @@ export const IssueGanttSidebar: React.FC = (props) => { ); -}; +}); diff --git a/web/components/gantt-chart/sidebar/modules/sidebar.tsx b/web/components/gantt-chart/sidebar/modules/sidebar.tsx index a4bcbd5ec..00e9ff21d 100644 --- a/web/components/gantt-chart/sidebar/modules/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/modules/sidebar.tsx @@ -2,22 +2,23 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea // ui import { Loader } from "@plane/ui"; // components -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; +import { ChartDataType, IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; import { ModulesSidebarBlock } from "./block"; // types type Props = { title: string; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - blocks: IGanttBlock[] | null; + getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; + blockIds: string[]; enableReorder: boolean; }; export const ModuleGanttSidebar: React.FC = (props) => { - const { blockUpdateHandler, blocks, enableReorder } = props; + const { blockUpdateHandler, blockIds, getBlockById, enableReorder } = props; const handleOrderChange = (result: DropResult) => { - if (!blocks) return; + if (!blockIds) return; const { source, destination } = result; @@ -27,29 +28,30 @@ export const ModuleGanttSidebar: React.FC = (props) => { // return if dropped on the same index if (source.index === destination.index) return; - let updatedSortOrder = blocks[source.index].sort_order; + let updatedSortOrder = getBlockById(blockIds[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; + if (destination.index === 0) updatedSortOrder = getBlockById(blockIds[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; + else if (destination.index === blockIds.length - 1) + updatedSortOrder = getBlockById(blockIds[blockIds.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 destinationSortingOrder = getBlockById(blockIds[destination.index]).sort_order; const relativeDestinationSortingOrder = source.index < destination.index - ? blocks[destination.index + 1].sort_order - : blocks[destination.index - 1].sort_order; + ? getBlockById(blockIds[destination.index + 1]).sort_order + : getBlockById(blockIds[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); + const removedElement = blockIds.splice(source.index, 1)[0]; + blockIds.splice(destination.index, 0, removedElement); // call the block update handler with the updated sort order, new and old index - blockUpdateHandler(removedElement.data, { + blockUpdateHandler(getBlockById(removedElement).data, { sort_order: { destinationIndex: destination.index, newSortOrder: updatedSortOrder, @@ -64,24 +66,27 @@ export const ModuleGanttSidebar: React.FC = (props) => { {(droppableProvided) => (
<> - {blocks ? ( - blocks.map((block, index) => ( - - {(provided, snapshot) => ( - - )} - - )) + {blockIds ? ( + blockIds.map((blockId, index) => { + const block = getBlockById(blockId); + return ( + + {(provided, snapshot) => ( + + )} + + ); + }) ) : ( diff --git a/web/components/gantt-chart/sidebar/root.tsx b/web/components/gantt-chart/sidebar/root.tsx index 0b877ba33..15b464d1f 100644 --- a/web/components/gantt-chart/sidebar/root.tsx +++ b/web/components/gantt-chart/sidebar/root.tsx @@ -1,19 +1,30 @@ // components -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; +import { ChartDataType, IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; // constants import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants"; type Props = { - blocks: IGanttBlock[] | null; + blockIds: string[]; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + loadMoreBlocks?: () => void; enableReorder: boolean; sidebarToRender: (props: any) => React.ReactNode; title: string; + getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; quickAdd?: React.JSX.Element | undefined; }; export const GanttChartSidebar: React.FC = (props) => { - const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title, quickAdd } = props; + const { + blockIds, + blockUpdateHandler, + enableReorder, + sidebarToRender, + getBlockById, + loadMoreBlocks, + title, + quickAdd, + } = props; return (
= (props) => {
- {sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })} + {sidebarToRender && + sidebarToRender({ title, blockUpdateHandler, blockIds, getBlockById, enableReorder, loadMoreBlocks })}
{quickAdd ? quickAdd : null}
diff --git a/web/components/gantt-chart/types/index.ts b/web/components/gantt-chart/types/index.ts index 6268e4363..cd90758fc 100644 --- a/web/components/gantt-chart/types/index.ts +++ b/web/components/gantt-chart/types/index.ts @@ -6,8 +6,8 @@ export interface IGanttBlock { width: number; }; sort_order: number; - start_date: Date | null; - target_date: Date | null; + start_date: Date | undefined; + target_date: Date | undefined; } export interface IBlockUpdateData { diff --git a/web/components/gantt-chart/views/month-view.ts b/web/components/gantt-chart/views/month-view.ts index 1e7e6d878..dcb11984a 100644 --- a/web/components/gantt-chart/views/month-view.ts +++ b/web/components/gantt-chart/views/month-view.ts @@ -167,7 +167,7 @@ export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, const { startDate } = chartData.data; const { start_date: itemStartDate, target_date: itemTargetDate } = itemData; - if (!itemStartDate || !itemTargetDate) return null; + if (!itemStartDate || !itemTargetDate) return; startDate.setHours(0, 0, 0, 0); itemStartDate.setHours(0, 0, 0, 0); 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 becda7096..693d9f677 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -1,20 +1,22 @@ -import React from "react"; +import React, { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks -import { GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "components/gantt-chart"; +import { ChartDataType, GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "components/gantt-chart"; import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues"; import { EUserProjectRoles } from "constants/project"; -import { renderIssueBlocksStructure } from "helpers/issue.helper"; +import { getIssueBlocksStructure } from "helpers/issue.helper"; import { useIssues, useUser } from "hooks/store"; import { useIssuesActions } from "hooks/use-issues-actions"; // components // helpers // types -import { TIssue, TUnGroupedIssues } from "@plane/types"; +import { TIssue, TIssueGroup } from "@plane/types"; // constants import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue"; import { IssueLayoutHOC } from "../issue-layout-HOC"; +import useSWR from "swr"; +import { getMonthChartItemPositionWidthInMonth } from "components/gantt-chart/views"; type GanttStoreType = | EIssuesStoreType.PROJECT @@ -32,19 +34,40 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan const router = useRouter(); const { workspaceSlug } = router.query; - const { issues, issuesFilter } = useIssues(storeType); - const { updateIssue } = useIssuesActions(storeType); + const { issues, issuesFilter, issueMap } = useIssues(storeType); + const { fetchIssues, fetchNextIssues, updateIssue } = useIssuesActions(storeType); // store hooks const { membership: { currentProjectRole }, } = useUser(); - const { issueMap } = useIssues(); const appliedDisplayFilters = issuesFilter.issueFilters?.displayFilters; - const issueIds = (issues.groupedIssueIds ?? []) as TUnGroupedIssues; + useSWR(`ISSUE_GANTT_LAYOUT_${storeType}`, () => fetchIssues("init-loader", { canGroup: false, perPageCount: 100 }), { + revalidateOnFocus: false, + revalidateOnReconnect: false, + }); + + const issuesObject = issues.groupedIssueIds?.["All Issues"] as TIssueGroup; const { enableIssueCreation } = issues?.viewFlags || {}; - const issuesArray = issueIds.map((id) => issueMap?.[id]); + const loadMoreIssues = useCallback(() => { + fetchNextIssues(); + }, [fetchNextIssues]); + + const getBlockById = useCallback( + (id: string, currentViewData?: ChartDataType | undefined) => { + const issue = issueMap[id]; + const block = getIssueBlocksStructure(issue); + if (currentViewData) { + return { + ...block, + position: getMonthChartItemPositionWidthInMonth(currentViewData, block), + }; + } + return block; + }, + [issueMap] + ); const updateIssueBlockStructure = async (issue: TIssue, data: IBlockUpdateData) => { if (!workspaceSlug) return; @@ -64,7 +87,8 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan border={false} title="Issues" loaderTitle="Issues" - blocks={issues ? renderIssueBlocksStructure(issuesArray) : null} + blockIds={issuesObject?.issueIds} + getBlockById={getBlockById} blockUpdateHandler={updateIssueBlockStructure} blockToRender={(data: TIssue) => } sidebarToRender={(props) => } @@ -78,6 +102,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan ) : undefined } + loadMoreBlocks={loadMoreIssues} showAllBlocks />
diff --git a/web/components/modules/gantt-chart/modules-list-layout.tsx b/web/components/modules/gantt-chart/modules-list-layout.tsx index 0a9b433c5..a6242ad04 100644 --- a/web/components/modules/gantt-chart/modules-list-layout.tsx +++ b/web/components/modules/gantt-chart/modules-list-layout.tsx @@ -2,11 +2,13 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store // components -import { GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "components/gantt-chart"; +import { ChartDataType, GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "components/gantt-chart"; import { ModuleGanttBlock } from "components/modules"; import { useModule, useProject } from "hooks/store"; // types import { IModule } from "@plane/types"; +import { useCallback } from "react"; +import { getMonthChartItemPositionWidthInMonth } from "components/gantt-chart/views"; export const ModulesListGanttChartView: React.FC = observer(() => { // router @@ -14,7 +16,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => { const { workspaceSlug } = router.query; // store const { currentProjectDetails } = useProject(); - const { projectModuleIds, moduleMap, updateModuleDetails } = useModule(); + const { projectModuleIds, getModuleById, updateModuleDetails } = useModule(); const handleModuleUpdate = async (module: IModule, data: IBlockUpdateData) => { if (!workspaceSlug || !module) return; @@ -25,26 +27,39 @@ export const ModulesListGanttChartView: React.FC = observer(() => { await updateModuleDetails(workspaceSlug.toString(), module.project_id, module.id, payload); }; - const blockFormat = (blocks: string[]) => - blocks?.map((blockId) => { - const block = moduleMap[blockId]; - return { - data: block, - id: block.id, - sort_order: block.sort_order, - start_date: block.start_date ? new Date(block.start_date) : null, - target_date: block.target_date ? new Date(block.target_date) : null, + const getBlockById = useCallback( + (id: string, currentViewData?: ChartDataType | undefined) => { + const module = getModuleById(id); + + const block = { + data: module, + id: module?.id ?? "", + sort_order: module?.sort_order ?? 0, + start_date: module?.start_date ? new Date(module.start_date) : undefined, + target_date: module?.target_date ? new Date(module.target_date) : undefined, }; - }); + if (currentViewData) { + return { + ...block, + position: getMonthChartItemPositionWidthInMonth(currentViewData, block), + }; + } + return block; + }, + [getModuleById] + ); const isAllowed = currentProjectDetails?.member_role === 20 || currentProjectDetails?.member_role === 15; + if (!projectModuleIds) return null; + return (
} blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)} blockToRender={(data: IModule) => } diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 7c8ba8554..39a954bc7 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -156,14 +156,15 @@ export const shouldHighlightIssueDueDate = ( // if the issue is overdue, highlight the due date return targetDateDistance <= 0; }; -export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => - blocks?.map((block) => ({ +export const getIssueBlocksStructure = (block: TIssue): IGanttBlock => { + return { data: block, id: block.id, sort_order: block.sort_order, - start_date: block.start_date ? new Date(block.start_date) : null, - target_date: block.target_date ? new Date(block.target_date) : null, - })); + start_date: block.start_date ? new Date(block.start_date) : undefined, + target_date: block.target_date ? new Date(block.target_date) : undefined, + }; +}; export function getChangedIssuefields(formData: Partial, dirtyFields: { [key: string]: boolean | undefined }) { const changedFields: Partial = {};