From 47abe9db5edf71421b84244be802288e775b4f5c Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 28 Aug 2023 13:25:47 +0530 Subject: [PATCH] dev: gantt chart revamp (#1900) * style: gantt chart polishing * chore: sidebar y-axis drag and drop * chore: remove y-axis drag and drop from the main content * refactor: drop end function * refactor: resizing logic * chore: x-axis block move * chore: x-axis block move flag * chore: update scroll end logic * style: modules gantt chart * style: block background tint * refactor: context dispatcher types * refactor: draggable component * chore: filters added to gantt chart * refactor: folder structure * style: cycle blocks * chore: move to block arrow * chore: move to block on the right side arrow * chore: added proper comments for functions * refactor: blocks render logic * fix: x-axis drag and drop * chore: minor ui fixes * chore: remove link tag from blocks --------- Co-authored-by: Aaryan Khandelwal --- .../core/filters/issues-view-filter.tsx | 66 +++-- apps/app/components/core/views/all-views.tsx | 5 +- .../components/cycles/gantt-chart/blocks.tsx | 83 ++++++ .../cycle-issues-layout.tsx} | 30 +-- .../cycles-list-layout.tsx} | 23 +- .../components/cycles/gantt-chart/index.ts | 3 + apps/app/components/cycles/index.ts | 3 +- .../components/cycles/single-cycle-list.tsx | 1 + .../components/gantt-chart/blocks/block.tsx | 103 -------- .../gantt-chart/blocks/blocks-display.tsx | 208 +++++---------- .../components/gantt-chart/blocks/index.ts | 1 - .../components/gantt-chart/chart/index.tsx | 147 +++++------ .../components/gantt-chart/chart/month.tsx | 50 ++-- .../components/gantt-chart/contexts/index.tsx | 13 +- apps/app/components/gantt-chart/data/index.ts | 4 +- .../gantt-chart/helpers/draggable.tsx | 249 +++++++++++++----- .../components/gantt-chart/hooks/index.tsx | 4 +- apps/app/components/gantt-chart/root.tsx | 34 ++- apps/app/components/gantt-chart/sidebar.tsx | 156 +++++++++++ .../app/components/gantt-chart/types/index.ts | 24 +- .../app/components/icons/state-group-icon.tsx | 5 + .../components/issues/gantt-chart/blocks.tsx | 67 +++++ .../components/issues/gantt-chart/index.ts | 2 + .../layout.tsx} | 23 +- .../components/modules/gantt-chart/blocks.tsx | 55 ++++ .../components/modules/gantt-chart/index.ts | 3 + .../module-issues-layout.tsx} | 30 +-- .../modules-list-layout.tsx} | 23 +- apps/app/components/modules/index.ts | 1 - apps/app/components/views/gantt-chart.tsx | 29 +- apps/app/constants/issue.ts | 1 + apps/app/helpers/date-time.helper.ts | 29 ++ .../hooks/gantt-chart/cycle-issues-view.tsx | 8 +- apps/app/hooks/gantt-chart/issue-view.tsx | 8 +- .../hooks/gantt-chart/module-issues-view.tsx | 8 +- .../hooks/gantt-chart/view-issues-view.tsx | 2 +- .../projects/[projectId]/modules/index.tsx | 95 ++++--- apps/app/types/issues.d.ts | 4 +- 38 files changed, 965 insertions(+), 635 deletions(-) create mode 100644 apps/app/components/cycles/gantt-chart/blocks.tsx rename apps/app/components/cycles/{gantt-chart.tsx => gantt-chart/cycle-issues-layout.tsx} (55%) rename apps/app/components/cycles/{cycles-list-gantt-chart.tsx => gantt-chart/cycles-list-layout.tsx} (75%) create mode 100644 apps/app/components/cycles/gantt-chart/index.ts delete mode 100644 apps/app/components/gantt-chart/blocks/block.tsx create mode 100644 apps/app/components/gantt-chart/sidebar.tsx create mode 100644 apps/app/components/issues/gantt-chart/blocks.tsx create mode 100644 apps/app/components/issues/gantt-chart/index.ts rename apps/app/components/issues/{gantt-chart.tsx => gantt-chart/layout.tsx} (59%) create mode 100644 apps/app/components/modules/gantt-chart/blocks.tsx create mode 100644 apps/app/components/modules/gantt-chart/index.ts rename apps/app/components/modules/{gantt-chart.tsx => gantt-chart/module-issues-layout.tsx} (57%) rename apps/app/components/modules/{modules-list-gantt-chart.tsx => gantt-chart/modules-list-layout.tsx} (74%) diff --git a/apps/app/components/core/filters/issues-view-filter.tsx b/apps/app/components/core/filters/issues-view-filter.tsx index 37aab34e995..3238a8d17f8 100644 --- a/apps/app/components/core/filters/issues-view-filter.tsx +++ b/apps/app/components/core/filters/issues-view-filter.tsx @@ -113,43 +113,41 @@ export const IssuesFilterView: React.FC = () => { ))} )} - {issueView !== "gantt_chart" && ( - { - const key = option.key as keyof typeof filters; + { + const key = option.key as keyof typeof filters; - if (key === "start_date" || key === "target_date") { - const valueExists = checkIfArraysHaveSameElements(filters[key] ?? [], option.value); + if (key === "start_date" || key === "target_date") { + const valueExists = checkIfArraysHaveSameElements(filters[key] ?? [], option.value); - setFilters({ - [key]: valueExists ? null : option.value, - }); - } else { - const valueExists = filters[key]?.includes(option.value); + setFilters({ + [key]: valueExists ? null : option.value, + }); + } else { + const valueExists = filters[key]?.includes(option.value); - if (valueExists) - setFilters( - { - [option.key]: ((filters[key] ?? []) as any[])?.filter( - (val) => val !== option.value - ), - }, - !Boolean(viewId) - ); - else - setFilters( - { - [option.key]: [...((filters[key] ?? []) as any[]), option.value], - }, - !Boolean(viewId) - ); - } - }} - direction="left" - height="rg" - /> - )} + if (valueExists) + setFilters( + { + [option.key]: ((filters[key] ?? []) as any[])?.filter( + (val) => val !== option.value + ), + }, + !Boolean(viewId) + ); + else + setFilters( + { + [option.key]: [...((filters[key] ?? []) as any[]), option.value], + }, + !Boolean(viewId) + ); + } + }} + direction="left" + height="rg" + /> {({ open }) => ( <> diff --git a/apps/app/components/core/views/all-views.tsx b/apps/app/components/core/views/all-views.tsx index 4a757649c45..79d5d6b11f0 100644 --- a/apps/app/components/core/views/all-views.tsx +++ b/apps/app/components/core/views/all-views.tsx @@ -114,7 +114,10 @@ export const AllViews: React.FC = ({ )} {groupedIssues ? ( - !isEmpty || issueView === "kanban" || issueView === "calendar" ? ( + !isEmpty || + issueView === "kanban" || + issueView === "calendar" || + issueView === "gantt_chart" ? ( <> {issueView === "list" ? ( { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const cycleStatus = getDateRangeStatus(data?.start_date, data?.end_date); + + return ( +
router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)} + > +
+ +
{data?.name}
+
+ {renderShortDate(data?.start_date ?? "")} to {renderShortDate(data?.end_date ?? "")} +
+
+ } + position="top-left" + > +
+ {data?.name} +
+ +
+ ); +}; + +export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const cycleStatus = getDateRangeStatus(data?.start_date, data?.end_date); + + return ( +
router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)} + > + +
{data?.name}
+
+ ); +}; diff --git a/apps/app/components/cycles/gantt-chart.tsx b/apps/app/components/cycles/gantt-chart/cycle-issues-layout.tsx similarity index 55% rename from apps/app/components/cycles/gantt-chart.tsx rename to apps/app/components/cycles/gantt-chart/cycle-issues-layout.tsx index fe276b50dad..7741432ceb6 100644 --- a/apps/app/components/cycles/gantt-chart.tsx +++ b/apps/app/components/cycles/gantt-chart/cycle-issues-layout.tsx @@ -6,11 +6,8 @@ import useUser from "hooks/use-user"; import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view"; import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; // components -import { - GanttChartRoot, - IssueGanttBlock, - renderIssueBlocksStructure, -} from "components/gantt-chart"; +import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart"; +import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; // types import { IIssue } from "types"; @@ -28,29 +25,20 @@ export const CycleIssuesGanttChartView = () => { cycleId as string ); - // rendering issues on gantt sidebar - const GanttSidebarBlockView = ({ data }: any) => ( -
-
-
{data?.name}
-
- ); - return ( -
+
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) } - sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + SidebarBlockRender={IssueGanttSidebarBlock} + BlockRender={IssueGanttBlock} enableReorder={orderBy === "sort_order"} + bottomSpacing />
); diff --git a/apps/app/components/cycles/cycles-list-gantt-chart.tsx b/apps/app/components/cycles/gantt-chart/cycles-list-layout.tsx similarity index 75% rename from apps/app/components/cycles/cycles-list-gantt-chart.tsx rename to apps/app/components/cycles/gantt-chart/cycles-list-layout.tsx index 4ad0029d830..a5b576bca70 100644 --- a/apps/app/components/cycles/cycles-list-gantt-chart.tsx +++ b/apps/app/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -9,7 +9,8 @@ import cyclesService from "services/cycles.service"; // hooks import useUser from "hooks/use-user"; // components -import { CycleGanttBlock, GanttChartRoot, IBlockUpdateData } from "components/gantt-chart"; +import { GanttChartRoot, IBlockUpdateData } from "components/gantt-chart"; +import { CycleGanttBlock, CycleGanttSidebarBlock } from "components/cycles"; // types import { ICycle } from "types"; @@ -24,17 +25,6 @@ export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => const { user } = useUser(); - // rendering issues on gantt sidebar - const GanttSidebarBlockView = ({ data }: any) => ( -
-
-
{data?.name}
-
- ); - const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => { if (!workspaceSlug || !user) return; @@ -88,10 +78,11 @@ export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => loaderTitle="Cycles" blocks={cycles ? blockFormat(cycles) : null} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} - sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } - enableLeftDrag={false} - enableRightDrag={false} + SidebarBlockRender={CycleGanttSidebarBlock} + BlockRender={CycleGanttBlock} + enableBlockLeftResize={false} + enableBlockRightResize={false} + enableBlockMove={false} />
); diff --git a/apps/app/components/cycles/gantt-chart/index.ts b/apps/app/components/cycles/gantt-chart/index.ts new file mode 100644 index 00000000000..e4850b2e259 --- /dev/null +++ b/apps/app/components/cycles/gantt-chart/index.ts @@ -0,0 +1,3 @@ +export * from "./blocks"; +export * from "./cycle-issues-layout"; +export * from "./cycles-list-layout"; diff --git a/apps/app/components/cycles/index.ts b/apps/app/components/cycles/index.ts index 40355d574c0..4b43b3a74db 100644 --- a/apps/app/components/cycles/index.ts +++ b/apps/app/components/cycles/index.ts @@ -1,11 +1,10 @@ export * from "./cycles-list"; export * from "./active-cycle-details"; export * from "./active-cycle-stats"; -export * from "./cycles-list-gantt-chart"; +export * from "./gantt-chart"; export * from "./cycles-view"; export * from "./delete-cycle-modal"; export * from "./form"; -export * from "./gantt-chart"; export * from "./modal"; export * from "./select"; export * from "./sidebar"; diff --git a/apps/app/components/cycles/single-cycle-list.tsx b/apps/app/components/cycles/single-cycle-list.tsx index 7518568edb0..ec01da9e760 100644 --- a/apps/app/components/cycles/single-cycle-list.tsx +++ b/apps/app/components/cycles/single-cycle-list.tsx @@ -106,6 +106,7 @@ function RadialProgressBar({ progress }: progress) {
); } + export const SingleCycleList: React.FC = ({ cycle, handleEditCycle, diff --git a/apps/app/components/gantt-chart/blocks/block.tsx b/apps/app/components/gantt-chart/blocks/block.tsx deleted file mode 100644 index 52fd1fe52fb..00000000000 --- a/apps/app/components/gantt-chart/blocks/block.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import Link from "next/link"; -import { useRouter } from "next/router"; - -// ui -import { Tooltip } from "components/ui"; -// helpers -import { renderShortDate } from "helpers/date-time.helper"; -// types -import { ICycle, IIssue, IModule } from "types"; -// constants -import { MODULE_STATUS } from "constants/module"; - -export const IssueGanttBlock = ({ issue }: { issue: IIssue }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - return ( - - -
- -
{issue.name}
-
- {renderShortDate(issue.start_date ?? "")} to{" "} - {renderShortDate(issue.target_date ?? "")} -
-
- } - position="top-left" - > -
- {issue.name} -
- -
- - ); -}; - -export const CycleGanttBlock = ({ cycle }: { cycle: ICycle }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - return ( - - -
- -
{cycle.name}
-
- {renderShortDate(cycle.start_date ?? "")} to {renderShortDate(cycle.end_date ?? "")} -
-
- } - position="top-left" - > -
- {cycle.name} -
- -
- - ); -}; - -export const ModuleGanttBlock = ({ module }: { module: IModule }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - return ( - - -
s.value === module.status)?.color }} - /> - -
{module.name}
-
- {renderShortDate(module.start_date ?? "")} to{" "} - {renderShortDate(module.target_date ?? "")} -
-
- } - position="top-left" - > -
- {module.name} -
- -
- - ); -}; diff --git a/apps/app/components/gantt-chart/blocks/blocks-display.tsx b/apps/app/components/gantt-chart/blocks/blocks-display.tsx index fd43c733ee3..f0e7279be65 100644 --- a/apps/app/components/gantt-chart/blocks/blocks-display.tsx +++ b/apps/app/components/gantt-chart/blocks/blocks-display.tsx @@ -1,8 +1,7 @@ import { FC } from "react"; -// react-beautiful-dnd -import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd"; -import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +// hooks +import { useChart } from "../hooks"; // helpers import { ChartDraggable } from "../helpers/draggable"; import { renderDateFormat } from "helpers/date-time.helper"; @@ -12,90 +11,59 @@ import { IBlockUpdateData, IGanttBlock } from "../types"; export const GanttChartBlocks: FC<{ itemsContainerWidth: number; blocks: IGanttBlock[] | null; - sidebarBlockRender: FC; - blockRender: FC; + BlockRender: React.FC; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - enableLeftDrag: boolean; - enableRightDrag: boolean; - enableReorder: boolean; + enableBlockLeftResize: boolean; + enableBlockRightResize: boolean; + enableBlockMove: boolean; }> = ({ itemsContainerWidth, blocks, - sidebarBlockRender, - blockRender, + BlockRender, blockUpdateHandler, - enableLeftDrag, - enableRightDrag, - enableReorder, + enableBlockLeftResize, + enableBlockRightResize, + enableBlockMove, }) => { + const { activeBlock, dispatch } = useChart(); + + // update the active block on hover + const updateActiveBlock = (block: IGanttBlock | null) => { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + activeBlock: block, + }, + }); + }; + const handleChartBlockPosition = ( block: IGanttBlock, totalBlockShifts: number, - dragDirection: "left" | "right" + dragDirection: "left" | "right" | "move" ) => { - let updatedDate = new Date(); - - if (dragDirection === "left") { - const originalDate = new Date(block.start_date); - - const currentDay = originalDate.getDate(); - updatedDate = new Date(originalDate); - - updatedDate.setDate(currentDay - totalBlockShifts); - } else { - const originalDate = new Date(block.target_date); - - const currentDay = originalDate.getDate(); - updatedDate = new Date(originalDate); - - updatedDate.setDate(currentDay + totalBlockShifts); + const originalStartDate = new Date(block.start_date); + const updatedStartDate = new Date(originalStartDate); + + const originalTargetDate = new Date(block.target_date); + const updatedTargetDate = new Date(originalTargetDate); + + // update the start date on left resize + if (dragDirection === "left") + updatedStartDate.setDate(originalStartDate.getDate() - totalBlockShifts); + // update the target date on right resize + else if (dragDirection === "right") + updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts); + // update both the dates on x-axis move + else if (dragDirection === "move") { + updatedStartDate.setDate(originalStartDate.getDate() + totalBlockShifts); + updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts); } + // call the block update handler with the updated dates blockUpdateHandler(block.data, { - [dragDirection === "left" ? "start_date" : "target_date"]: renderDateFormat(updatedDate), - }); - }; - - const handleOrderChange = (result: DropResult) => { - if (!blocks) return; - - const { source, destination, draggableId } = result; - - if (!destination) return; - - if (source.index === destination.index && document) { - // const draggedBlock = document.querySelector(`#${draggableId}`) as HTMLElement; - // const blockStyles = window.getComputedStyle(draggedBlock); - - // console.log(blockStyles.marginLeft); - - return; - } - - let updatedSortOrder = blocks[source.index].sort_order; - - if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; - else if (destination.index === blocks.length - 1) - updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; - 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; - } - - const removedElement = blocks.splice(source.index, 1)[0]; - blocks.splice(destination.index, 0, removedElement); - - blockUpdateHandler(removedElement.data, { - sort_order: { - destinationIndex: destination.index, - newSortOrder: updatedSortOrder, - sourceIndex: source.index, - }, + start_date: renderDateFormat(updatedStartDate), + target_date: renderDateFormat(updatedTargetDate), }); }; @@ -104,75 +72,29 @@ export const GanttChartBlocks: FC<{ className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto" style={{ width: `${itemsContainerWidth}px` }} > - - - {(droppableProvided, droppableSnapshot) => ( -
- <> - {blocks && - blocks.length > 0 && - blocks.map( - (block, index: number) => - block.start_date && - block.target_date && ( - - {(provided) => ( -
- handleChartBlockPosition(block, ...args)} - enableLeftDrag={enableLeftDrag} - enableRightDrag={enableRightDrag} - provided={provided} - > -
- {blockRender({ - ...block.data, - })} -
-
-
- )} -
- ) - )} - {droppableProvided.placeholder} - -
- )} -
-
- - {/* sidebar */} - {/*
- {blocks && - blocks.length > 0 && - blocks.map((block: any, _idx: number) => ( -
- {sidebarBlockRender(block?.data)} -
- ))} -
*/} + {blocks && + blocks.length > 0 && + blocks.map( + (block) => + block.start_date && + block.target_date && ( +
updateActiveBlock(block)} + onMouseLeave={() => updateActiveBlock(null)} + > + handleChartBlockPosition(block, ...args)} + enableBlockLeftResize={enableBlockLeftResize} + enableBlockRightResize={enableBlockRightResize} + enableBlockMove={enableBlockMove} + /> +
+ ) + )}
); }; diff --git a/apps/app/components/gantt-chart/blocks/index.ts b/apps/app/components/gantt-chart/blocks/index.ts index 8773b2797c4..18ca5da9e86 100644 --- a/apps/app/components/gantt-chart/blocks/index.ts +++ b/apps/app/components/gantt-chart/blocks/index.ts @@ -1,2 +1 @@ -export * from "./block"; export * from "./blocks-display"; diff --git a/apps/app/components/gantt-chart/chart/index.tsx b/apps/app/components/gantt-chart/chart/index.tsx index 0cc8a14eec3..aa79ae19c8e 100644 --- a/apps/app/components/gantt-chart/chart/index.tsx +++ b/apps/app/components/gantt-chart/chart/index.tsx @@ -3,6 +3,7 @@ import { FC, useEffect, useState } from "react"; import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/20/solid"; // components import { GanttChartBlocks } from "components/gantt-chart"; +import { GanttSidebar } from "../sidebar"; // import { HourChartView } from "./hours"; // import { DayChartView } from "./day"; // import { WeekChartView } from "./week"; @@ -25,7 +26,7 @@ import { getMonthChartItemPositionWidthInMonth, } from "../views"; // types -import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types"; +import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types"; // data import { currentViewDataWithView } from "../data"; // context @@ -33,15 +34,17 @@ import { useChart } from "../hooks"; type ChartViewRootProps = { border: boolean; - title: null | string; + title: string; loaderTitle: string; blocks: IGanttBlock[] | null; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - sidebarBlockRender: FC; - blockRender: FC; - enableLeftDrag: boolean; - enableRightDrag: boolean; + SidebarBlockRender: React.FC; + BlockRender: React.FC; + enableBlockLeftResize: boolean; + enableBlockRightResize: boolean; + enableBlockMove: boolean; enableReorder: boolean; + bottomSpacing: boolean; }; export const ChartViewRoot: FC = ({ @@ -50,22 +53,24 @@ export const ChartViewRoot: FC = ({ blocks = null, loaderTitle, blockUpdateHandler, - sidebarBlockRender, - blockRender, - enableLeftDrag, - enableRightDrag, + SidebarBlockRender, + BlockRender, + enableBlockLeftResize, + enableBlockRightResize, + enableBlockMove, enableReorder, + bottomSpacing, }) => { - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); - const [itemsContainerWidth, setItemsContainerWidth] = useState(0); const [fullScreenMode, setFullScreenMode] = useState(false); - const [blocksSidebarView, setBlocksSidebarView] = useState(false); // blocks state management starts const [chartBlocks, setChartBlocks] = useState(null); - const renderBlockStructure = (view: any, blocks: IGanttBlock[]) => + const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = + useChart(); + + const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) => blocks && blocks.length > 0 ? blocks.map((block: any) => ({ ...block, @@ -74,16 +79,16 @@ export const ChartViewRoot: FC = ({ : []; useEffect(() => { - if (currentViewData && blocks && blocks.length > 0) + if (currentViewData && blocks) setChartBlocks(() => renderBlockStructure(currentViewData, blocks)); }, [currentViewData, blocks]); // blocks state management ends - const handleChartView = (key: string) => updateCurrentViewRenderPayload(null, key); + const handleChartView = (key: TGanttViews) => updateCurrentViewRenderPayload(null, key); - const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: string) => { - const selectedCurrentView = view; + const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => { + const selectedCurrentView: TGanttViews = view; const selectedCurrentViewData: ChartDataType | undefined = selectedCurrentView && selectedCurrentView === currentViewData?.key ? currentViewData @@ -155,6 +160,9 @@ export const ChartViewRoot: FC = ({ const updatingCurrentLeftScrollPosition = (width: number) => { const scrollContainer = document.getElementById("scroll-container") as HTMLElement; + + if (!scrollContainer) return; + scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft; setItemsContainerWidth(width + scrollContainer?.scrollLeft); }; @@ -195,6 +203,8 @@ export const ChartViewRoot: FC = ({ 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); @@ -205,16 +215,6 @@ export const ChartViewRoot: FC = ({ updateCurrentViewRenderPayload("left", currentView); }; - useEffect(() => { - const scrollContainer = document.getElementById("scroll-container") as HTMLElement; - - scrollContainer.addEventListener("scroll", onScroll); - - return () => { - scrollContainer.removeEventListener("scroll", onScroll); - }; - }, [renderView]); - return (
= ({ border ? `border border-custom-border-200` : `` } flex h-full flex-col rounded-sm select-none bg-custom-background-100 shadow`} > - {/* chart title */} - {/*
- {title && ( -
-
{title}
-
- Gantt View Beta -
-
- )} - {blocks === null ? ( -
Loading...
- ) : ( -
- {blocks.length} {loaderTitle} -
- )} -
*/} - {/* chart header */} -
- {/*
setBlocksSidebarView(() => !blocksSidebarView)} - > - {blocksSidebarView ? ( - - ) : ( - - )} -
*/} - +
{title && (
{title}
-
+ {/*
Gantt View Beta -
+
*/}
)} @@ -282,7 +252,7 @@ export const ChartViewRoot: FC = ({ allViews.map((_chatView: any, _idx: any) => (
= ({
Today @@ -316,26 +286,30 @@ export const ChartViewRoot: FC = ({
{/* content */} -
+
+
+
+ +
- {/* blocks components */} - {currentView && currentViewData && ( - - )} - - {/* chart */} {/* {currentView && currentView === "hours" && } */} {/* {currentView && currentView === "day" && } */} {/* {currentView && currentView === "week" && } */} @@ -343,6 +317,19 @@ export const ChartViewRoot: FC = ({ {currentView && currentView === "month" && } {/* {currentView && currentView === "quarter" && } */} {/* {currentView && currentView === "year" && } */} + + {/* blocks */} + {currentView && currentViewData && ( + + )}
diff --git a/apps/app/components/gantt-chart/chart/month.tsx b/apps/app/components/gantt-chart/chart/month.tsx index b6c68b5d116..2a4a67daf93 100644 --- a/apps/app/components/gantt-chart/chart/month.tsx +++ b/apps/app/components/gantt-chart/chart/month.tsx @@ -17,9 +17,38 @@ export const MonthChartView: FC = () => { monthBlocks.length > 0 && monthBlocks.map((block, _idxRoot) => (
-
-
- {block?.title} +
+
+
+ {block?.title} +
+
+ +
+ {block?.children && + block?.children.length > 0 && + block?.children.map((monthDay, _idx) => ( +
+
+ + {monthDay.dayData.shortTitle[0]} + {" "} + + {monthDay.day} + +
+
+ ))}
@@ -28,19 +57,10 @@ export const MonthChartView: FC = () => { block?.children.length > 0 && block?.children.map((monthDay, _idx) => (
-
-
{monthDay?.title}
-
= () => { : `` }`} > - {monthDay?.today && ( + {/* {monthDay?.today && (
- )} + )} */}
))} diff --git a/apps/app/components/gantt-chart/contexts/index.tsx b/apps/app/components/gantt-chart/contexts/index.tsx index 4f858cf336c..05dfbe67879 100644 --- a/apps/app/components/gantt-chart/contexts/index.tsx +++ b/apps/app/components/gantt-chart/contexts/index.tsx @@ -32,16 +32,27 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ currentViewData: currentViewDataWithView(initialView), renderView: [], allViews: allViewsWithData, + activeBlock: null, }); + const [scrollLeft, setScrollLeft] = useState(0); + const handleDispatch = (action: ChartContextActionPayload): ChartContextData => { const newState = chartReducer(state, action); + dispatch(() => newState); + return newState; }; + const updateScrollLeft = (scrollLeft: number) => { + setScrollLeft(scrollLeft); + }; + return ( - + {children} ); diff --git a/apps/app/components/gantt-chart/data/index.ts b/apps/app/components/gantt-chart/data/index.ts index 4be79999812..4e1921434ad 100644 --- a/apps/app/components/gantt-chart/data/index.ts +++ b/apps/app/components/gantt-chart/data/index.ts @@ -108,8 +108,8 @@ export const allViewsWithData: ChartDataType[] = [ startDate: new Date(), currentDate: new Date(), endDate: new Date(), - approxFilterRange: 8, - width: 80, // it will preview monthly all dates with weekends highlighted with no limitations ex: title (1, 2, 3) + approxFilterRange: 6, + width: 55, // it will preview monthly all dates with weekends highlighted with no limitations ex: title (1, 2, 3) }, }, // { diff --git a/apps/app/components/gantt-chart/helpers/draggable.tsx b/apps/app/components/gantt-chart/helpers/draggable.tsx index 320f4355fee..20423ff5905 100644 --- a/apps/app/components/gantt-chart/helpers/draggable.tsx +++ b/apps/app/components/gantt-chart/helpers/draggable.tsx @@ -1,45 +1,57 @@ -import React, { useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; -// react-beautiful-dnd -import { DraggableProvided } from "react-beautiful-dnd"; +// icons +import { Icon } from "components/ui"; +// hooks import { useChart } from "../hooks"; // types import { IGanttBlock } from "../types"; type Props = { - children: any; block: IGanttBlock; - handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right") => void; - enableLeftDrag: boolean; - enableRightDrag: boolean; - provided: DraggableProvided; + BlockRender: React.FC; + handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right" | "move") => void; + enableBlockLeftResize: boolean; + enableBlockRightResize: boolean; + enableBlockMove: boolean; }; export const ChartDraggable: React.FC = ({ - children, block, + BlockRender, handleBlock, - enableLeftDrag = true, - enableRightDrag = true, - provided, + enableBlockLeftResize, + enableBlockRightResize, + enableBlockMove, }) => { const [isLeftResizing, setIsLeftResizing] = useState(false); const [isRightResizing, setIsRightResizing] = useState(false); + const [isMoving, setIsMoving] = useState(false); + const [posFromLeft, setPosFromLeft] = useState(null); - const parentDivRef = useRef(null); const resizableRef = useRef(null); - const { currentViewData } = useChart(); + const { currentViewData, scrollLeft } = useChart(); + // check if cursor reaches either end while resizing/dragging const checkScrollEnd = (e: MouseEvent): number => { + const SCROLL_THRESHOLD = 70; + let delWidth = 0; + const ganttContainer = document.querySelector("#gantt-container") as HTMLElement; + const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLElement; + const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; - const appSidebar = document.querySelector("#app-sidebar") as HTMLElement; + + if (!ganttContainer || !ganttSidebar || !scrollContainer) return 0; const posFromLeft = e.clientX; // manually scroll to left if reached the left end while dragging - if (posFromLeft - appSidebar.clientWidth <= 70) { + if ( + posFromLeft - (ganttContainer.getBoundingClientRect().left + ganttSidebar.clientWidth) <= + SCROLL_THRESHOLD + ) { if (e.movementX > 0) return 0; delWidth = -5; @@ -48,8 +60,8 @@ export const ChartDraggable: React.FC = ({ } else delWidth = e.movementX; // manually scroll to right if reached the right end while dragging - const posFromRight = window.innerWidth - e.clientX; - if (posFromRight <= 70) { + const posFromRight = ganttContainer.getBoundingClientRect().right - e.clientX; + if (posFromRight <= SCROLL_THRESHOLD) { if (e.movementX < 0) return 0; delWidth = 5; @@ -60,12 +72,11 @@ export const ChartDraggable: React.FC = ({ return delWidth; }; - const handleLeftDrag = () => { - if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position) - return; + // handle block resize from the left end + const handleBlockLeftResize = () => { + if (!currentViewData || !resizableRef.current || !block.position) return; const resizableDiv = resizableRef.current; - const parentDiv = parentDivRef.current; const columnWidth = currentViewData.data.width; @@ -73,11 +84,9 @@ export const ChartDraggable: React.FC = ({ resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); - let initialMarginLeft = parseInt(parentDiv.style.marginLeft); + let initialMarginLeft = parseInt(resizableDiv.style.marginLeft); const handleMouseMove = (e: MouseEvent) => { - if (!window) return; - let delWidth = 0; delWidth = checkScrollEnd(e); @@ -92,7 +101,7 @@ export const ChartDraggable: React.FC = ({ if (newWidth < columnWidth) return; resizableDiv.style.width = `${newWidth}px`; - parentDiv.style.marginLeft = `${newMarginLeft}px`; + resizableDiv.style.marginLeft = `${newMarginLeft}px`; if (block.position) { block.position.width = newWidth; @@ -100,6 +109,7 @@ export const ChartDraggable: React.FC = ({ } }; + // remove event listeners and call block handler with the updated start date const handleMouseUp = () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); @@ -115,9 +125,9 @@ export const ChartDraggable: React.FC = ({ document.addEventListener("mouseup", handleMouseUp); }; - const handleRightDrag = () => { - if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position) - return; + // handle block resize from the right end + const handleBlockRightResize = () => { + if (!currentViewData || !resizableRef.current || !block.position) return; const resizableDiv = resizableRef.current; @@ -129,8 +139,6 @@ export const ChartDraggable: React.FC = ({ let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); const handleMouseMove = (e: MouseEvent) => { - if (!window) return; - let delWidth = 0; delWidth = checkScrollEnd(e); @@ -145,6 +153,7 @@ export const ChartDraggable: React.FC = ({ if (block.position) block.position.width = Math.max(newWidth, 80); }; + // remove event listeners and call block handler with the updated target date const handleMouseUp = () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); @@ -160,46 +169,148 @@ export const ChartDraggable: React.FC = ({ document.addEventListener("mouseup", handleMouseUp); }; + // handle block x-axis move + const handleBlockMove = (e: React.MouseEvent) => { + if (!enableBlockMove || !currentViewData || !resizableRef.current || !block.position) return; + + e.preventDefault(); + e.stopPropagation(); + + setIsMoving(true); + + const resizableDiv = resizableRef.current; + + const columnWidth = currentViewData.data.width; + + const blockInitialMarginLeft = parseInt(resizableDiv.style.marginLeft); + + let initialMarginLeft = parseInt(resizableDiv.style.marginLeft); + + const handleMouseMove = (e: MouseEvent) => { + let delWidth = 0; + + delWidth = checkScrollEnd(e); + + // calculate new marginLeft and update the initial marginLeft using -= + const newMarginLeft = Math.round((initialMarginLeft += delWidth) / columnWidth) * columnWidth; + + resizableDiv.style.marginLeft = `${newMarginLeft}px`; + + if (block.position) block.position.marginLeft = newMarginLeft; + }; + + // remove event listeners and call block handler with the updated dates + const handleMouseUp = () => { + setIsMoving(false); + + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + + const totalBlockShifts = Math.ceil( + (parseInt(resizableDiv.style.marginLeft) - blockInitialMarginLeft) / columnWidth + ); + + handleBlock(totalBlockShifts, "move"); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }; + + // scroll to a hidden block + const handleScrollToBlock = () => { + const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; + + 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; + return ( -
- {enableLeftDrag && ( - <> -
setIsLeftResizing(true)} - onMouseLeave={() => setIsLeftResizing(false)} - className="absolute top-1/2 -left-2.5 -translate-y-1/2 z-[1] w-6 h-10 bg-brand-backdrop rounded-md cursor-col-resize" - /> -
- + <> + {/* move to left side hidden block button */} + {isBlockHiddenOnLeft && ( +
+ +
)} - {React.cloneElement(children, { ref: resizableRef, ...provided.dragHandleProps })} - {enableRightDrag && ( - <> -
setIsRightResizing(true)} - onMouseLeave={() => setIsRightResizing(false)} - className="absolute top-1/2 -right-2.5 -translate-y-1/2 z-[1] w-6 h-6 bg-brand-backdrop rounded-md cursor-col-resize" - /> -
- + {/* move to right side hidden block button */} + {isBlockHiddenOnRight && ( +
+ +
)} -
+
+ {/* left resize drag handle */} + {enableBlockLeftResize && ( + <> +
setIsLeftResizing(true)} + onMouseLeave={() => setIsLeftResizing(false)} + className="absolute top-1/2 -left-2.5 -translate-y-1/2 z-[3] w-6 h-full rounded-md cursor-col-resize" + /> +
+ + )} +
+ +
+ {/* right resize drag handle */} + {enableBlockRightResize && ( + <> +
setIsRightResizing(true)} + onMouseLeave={() => setIsRightResizing(false)} + className="absolute top-1/2 -right-2.5 -translate-y-1/2 z-[2] w-6 h-full rounded-md cursor-col-resize" + /> +
+ + )} +
+ ); }; diff --git a/apps/app/components/gantt-chart/hooks/index.tsx b/apps/app/components/gantt-chart/hooks/index.tsx index a26b56f84d4..5fb9bee3f40 100644 --- a/apps/app/components/gantt-chart/hooks/index.tsx +++ b/apps/app/components/gantt-chart/hooks/index.tsx @@ -7,9 +7,7 @@ import { ChartContext } from "../contexts"; export const useChart = (): ChartContextReducer => { const context = useContext(ChartContext); - if (!context) { - throw new Error("useChart must be used within a GanttChart"); - } + if (!context) throw new Error("useChart must be used within a GanttChart"); return context; }; diff --git a/apps/app/components/gantt-chart/root.tsx b/apps/app/components/gantt-chart/root.tsx index 077e8a896b7..5acedd53e59 100644 --- a/apps/app/components/gantt-chart/root.tsx +++ b/apps/app/components/gantt-chart/root.tsx @@ -8,28 +8,32 @@ import { IBlockUpdateData, IGanttBlock } from "./types"; type GanttChartRootProps = { border?: boolean; - title: null | string; + title: string; loaderTitle: string; blocks: IGanttBlock[] | null; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - sidebarBlockRender: FC; - blockRender: FC; - enableLeftDrag?: boolean; - enableRightDrag?: boolean; + SidebarBlockRender: FC; + BlockRender: FC; + enableBlockLeftResize?: boolean; + enableBlockRightResize?: boolean; + enableBlockMove?: boolean; enableReorder?: boolean; + bottomSpacing?: boolean; }; export const GanttChartRoot: FC = ({ border = true, - title = null, + title, blocks, loaderTitle = "blocks", blockUpdateHandler, - sidebarBlockRender, - blockRender, - enableLeftDrag = true, - enableRightDrag = true, + SidebarBlockRender, + BlockRender, + enableBlockLeftResize = true, + enableBlockRightResize = true, + enableBlockMove = true, enableReorder = true, + bottomSpacing = false, }) => ( = ({ blocks={blocks} loaderTitle={loaderTitle} blockUpdateHandler={blockUpdateHandler} - sidebarBlockRender={sidebarBlockRender} - blockRender={blockRender} - enableLeftDrag={enableLeftDrag} - enableRightDrag={enableRightDrag} + SidebarBlockRender={SidebarBlockRender} + BlockRender={BlockRender} + enableBlockLeftResize={enableBlockLeftResize} + enableBlockRightResize={enableBlockRightResize} + enableBlockMove={enableBlockMove} enableReorder={enableReorder} + bottomSpacing={bottomSpacing} /> ); diff --git a/apps/app/components/gantt-chart/sidebar.tsx b/apps/app/components/gantt-chart/sidebar.tsx new file mode 100644 index 00000000000..92e7a603d9f --- /dev/null +++ b/apps/app/components/gantt-chart/sidebar.tsx @@ -0,0 +1,156 @@ +// react-beautiful-dnd +import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd"; +import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +// hooks +import { useChart } from "./hooks"; +// ui +import { Loader } from "components/ui"; +// icons +import { EllipsisVerticalIcon } from "@heroicons/react/24/outline"; +// types +import { IBlockUpdateData, IGanttBlock } from "./types"; + +type Props = { + title: string; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + blocks: IGanttBlock[] | null; + SidebarBlockRender: React.FC; + enableReorder: boolean; +}; + +export const GanttSidebar: React.FC = ({ + title, + blockUpdateHandler, + blocks, + SidebarBlockRender, + enableReorder, +}) => { + const { activeBlock, dispatch } = useChart(); + + // update the active block on hover + const updateActiveBlock = (block: IGanttBlock | null) => { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + activeBlock: block, + }, + }); + }; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination } = result; + + // return if dropped outside the list + if (!destination) return; + + // return if dropped on the same index + if (source.index === destination.index) return; + + let updatedSortOrder = blocks[source.index].sort_order; + + // update the sort order to the lowest if dropped at the top + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + // update the sort order to the highest if dropped at the bottom + else if (destination.index === blocks.length - 1) + updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + // update the sort order to the average of the two adjacent blocks if dropped in between + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + // extract the element from the source index and insert it at the destination index without updating the entire array + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + // call the block update handler with the updated sort order, new and old index + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( + + + {(droppableProvided) => ( +
+ <> + {blocks ? ( + blocks.length > 0 ? ( + blocks.map((block, index) => ( + + {(provided, snapshot) => ( +
updateActiveBlock(block)} + onMouseLeave={() => updateActiveBlock(null)} + ref={provided.innerRef} + {...provided.draggableProps} + > +
+ {enableReorder && ( + + )} +
+ +
+
+
+ )} +
+ )) + ) : ( +
+ No {title} found +
+ ) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/apps/app/components/gantt-chart/types/index.ts b/apps/app/components/gantt-chart/types/index.ts index 645fd9c87fb..9cab40f5cc2 100644 --- a/apps/app/components/gantt-chart/types/index.ts +++ b/apps/app/components/gantt-chart/types/index.ts @@ -27,19 +27,33 @@ export interface IBlockUpdateData { target_date?: string; } +export type TGanttViews = "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year"; + export interface ChartContextData { allViews: allViewsType[]; - currentView: "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year"; + currentView: TGanttViews; currentViewData: ChartDataType | undefined; renderView: any; + activeBlock: IGanttBlock | null; } -export type ChartContextActionPayload = { - type: "CURRENT_VIEW" | "CURRENT_VIEW_DATA" | "PARTIAL_UPDATE" | "RENDER_VIEW"; - payload: any; -}; +export type ChartContextActionPayload = + | { + type: "CURRENT_VIEW"; + payload: TGanttViews; + } + | { + type: "CURRENT_VIEW_DATA" | "RENDER_VIEW"; + payload: ChartDataType | undefined; + } + | { + type: "PARTIAL_UPDATE"; + payload: Partial; + }; export interface ChartContextReducer extends ChartContextData { + scrollLeft: number; + updateScrollLeft: (scrollLeft: number) => void; dispatch: (action: ChartContextActionPayload) => void; } diff --git a/apps/app/components/icons/state-group-icon.tsx b/apps/app/components/icons/state-group-icon.tsx index b7da7136f13..522e0b9dc40 100644 --- a/apps/app/components/icons/state-group-icon.tsx +++ b/apps/app/components/icons/state-group-icon.tsx @@ -21,6 +21,7 @@ export const getStateGroupIcon = ( width={width} height={height} color={color ?? STATE_GROUP_COLORS["backlog"]} + className="flex-shrink-0" /> ); case "unstarted": @@ -29,6 +30,7 @@ export const getStateGroupIcon = ( width={width} height={height} color={color ?? STATE_GROUP_COLORS["unstarted"]} + className="flex-shrink-0" /> ); case "started": @@ -37,6 +39,7 @@ export const getStateGroupIcon = ( width={width} height={height} color={color ?? STATE_GROUP_COLORS["started"]} + className="flex-shrink-0" /> ); case "completed": @@ -45,6 +48,7 @@ export const getStateGroupIcon = ( width={width} height={height} color={color ?? STATE_GROUP_COLORS["completed"]} + className="flex-shrink-0" /> ); case "cancelled": @@ -53,6 +57,7 @@ export const getStateGroupIcon = ( width={width} height={height} color={color ?? STATE_GROUP_COLORS["cancelled"]} + className="flex-shrink-0" /> ); default: diff --git a/apps/app/components/issues/gantt-chart/blocks.tsx b/apps/app/components/issues/gantt-chart/blocks.tsx new file mode 100644 index 00000000000..2ad21c49969 --- /dev/null +++ b/apps/app/components/issues/gantt-chart/blocks.tsx @@ -0,0 +1,67 @@ +import { useRouter } from "next/router"; + +// ui +import { Tooltip } from "components/ui"; +// icons +import { getStateGroupIcon } from "components/icons"; +// helpers +import { findTotalDaysInRange, renderShortDate } from "helpers/date-time.helper"; +// types +import { IIssue } from "types"; + +export const IssueGanttBlock = ({ data }: { data: IIssue }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + return ( +
router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)} + > +
+ +
{data?.name}
+
+ {renderShortDate(data?.start_date ?? "")} to{" "} + {renderShortDate(data?.target_date ?? "")} +
+
+ } + position="top-left" + > +
+ {data?.name} +
+ +
+ ); +}; + +// rendering issues on gantt sidebar +export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const duration = findTotalDaysInRange(data?.start_date ?? "", data?.target_date ?? "", true); + + return ( +
router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)} + > + {getStateGroupIcon(data?.state_detail?.group, "14", "14", data?.state_detail?.color)} +
+ {data?.project_detail?.identifier} {data?.sequence_id} +
+
+
{data?.name}
+ + {duration} day{duration > 1 ? "s" : ""} + +
+
+ ); +}; diff --git a/apps/app/components/issues/gantt-chart/index.ts b/apps/app/components/issues/gantt-chart/index.ts new file mode 100644 index 00000000000..7f0d162739a --- /dev/null +++ b/apps/app/components/issues/gantt-chart/index.ts @@ -0,0 +1,2 @@ +export * from "./blocks"; +export * from "./layout"; diff --git a/apps/app/components/issues/gantt-chart.tsx b/apps/app/components/issues/gantt-chart/layout.tsx similarity index 59% rename from apps/app/components/issues/gantt-chart.tsx rename to apps/app/components/issues/gantt-chart/layout.tsx index 4912183a894..a42d764d820 100644 --- a/apps/app/components/issues/gantt-chart.tsx +++ b/apps/app/components/issues/gantt-chart/layout.tsx @@ -6,11 +6,8 @@ import useUser from "hooks/use-user"; import useGanttChartIssues from "hooks/gantt-chart/issue-view"; import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; // components -import { - GanttChartRoot, - IssueGanttBlock, - renderIssueBlocksStructure, -} from "components/gantt-chart"; +import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart"; +import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; // types import { IIssue } from "types"; @@ -27,17 +24,6 @@ export const IssueGanttChartView = () => { projectId as string ); - // rendering issues on gantt sidebar - const GanttSidebarBlockView = ({ data }: any) => ( -
-
-
{data?.name}
-
- ); - return (
{ blockUpdateHandler={(block, payload) => updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) } - sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + BlockRender={IssueGanttBlock} + SidebarBlockRender={IssueGanttSidebarBlock} enableReorder={orderBy === "sort_order"} + bottomSpacing />
); diff --git a/apps/app/components/modules/gantt-chart/blocks.tsx b/apps/app/components/modules/gantt-chart/blocks.tsx new file mode 100644 index 00000000000..bcf30709890 --- /dev/null +++ b/apps/app/components/modules/gantt-chart/blocks.tsx @@ -0,0 +1,55 @@ +import { useRouter } from "next/router"; + +// ui +import { Tooltip } from "components/ui"; +// helpers +import { renderShortDate } from "helpers/date-time.helper"; +// types +import { IModule } from "types"; +// constants +import { MODULE_STATUS } from "constants/module"; + +export const ModuleGanttBlock = ({ data }: { data: IModule }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + return ( +
s.value === data?.status)?.color }} + onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data?.id}`)} + > +
+ +
{data?.name}
+
+ {renderShortDate(data?.start_date ?? "")} to{" "} + {renderShortDate(data?.target_date ?? "")} +
+
+ } + position="top-left" + > +
+ {data?.name} +
+ +
+ ); +}; + +export const ModuleGanttSidebarBlock = ({ data }: { data: IModule }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + return ( +
router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data.id}`)} + > +
{data.name}
+
+ ); +}; diff --git a/apps/app/components/modules/gantt-chart/index.ts b/apps/app/components/modules/gantt-chart/index.ts new file mode 100644 index 00000000000..301b9f8405e --- /dev/null +++ b/apps/app/components/modules/gantt-chart/index.ts @@ -0,0 +1,3 @@ +export * from "./blocks"; +export * from "./module-issues-layout"; +export * from "./modules-list-layout"; diff --git a/apps/app/components/modules/gantt-chart.tsx b/apps/app/components/modules/gantt-chart/module-issues-layout.tsx similarity index 57% rename from apps/app/components/modules/gantt-chart.tsx rename to apps/app/components/modules/gantt-chart/module-issues-layout.tsx index 8ab8b60245c..9c0b05078c9 100644 --- a/apps/app/components/modules/gantt-chart.tsx +++ b/apps/app/components/modules/gantt-chart/module-issues-layout.tsx @@ -8,11 +8,8 @@ import useUser from "hooks/use-user"; import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view"; import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; // components -import { - GanttChartRoot, - IssueGanttBlock, - renderIssueBlocksStructure, -} from "components/gantt-chart"; +import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart"; +import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; // types import { IIssue } from "types"; @@ -32,29 +29,20 @@ export const ModuleIssuesGanttChartView: FC = ({}) => { moduleId as string ); - // rendering issues on gantt sidebar - const GanttSidebarBlockView = ({ data }: any) => ( -
-
-
{data?.name}
-
- ); - return ( -
+
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) } - sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + SidebarBlockRender={IssueGanttSidebarBlock} + BlockRender={IssueGanttBlock} enableReorder={orderBy === "sort_order"} + bottomSpacing />
); diff --git a/apps/app/components/modules/modules-list-gantt-chart.tsx b/apps/app/components/modules/gantt-chart/modules-list-layout.tsx similarity index 74% rename from apps/app/components/modules/modules-list-gantt-chart.tsx rename to apps/app/components/modules/gantt-chart/modules-list-layout.tsx index 7dc281fb3f7..70f493dde17 100644 --- a/apps/app/components/modules/modules-list-gantt-chart.tsx +++ b/apps/app/components/modules/gantt-chart/modules-list-layout.tsx @@ -1,6 +1,7 @@ import { FC } from "react"; import { useRouter } from "next/router"; +import Link from "next/link"; import { KeyedMutator } from "swr"; @@ -9,11 +10,10 @@ import modulesService from "services/modules.service"; // hooks import useUser from "hooks/use-user"; // components -import { GanttChartRoot, IBlockUpdateData, ModuleGanttBlock } from "components/gantt-chart"; +import { GanttChartRoot, IBlockUpdateData } from "components/gantt-chart"; +import { ModuleGanttBlock, ModuleGanttSidebarBlock } from "components/modules"; // types import { IModule } from "types"; -// constants -import { MODULE_STATUS } from "constants/module"; type Props = { modules: IModule[]; @@ -26,19 +26,6 @@ export const ModulesListGanttChartView: FC = ({ modules, mutateModules }) const { user } = useUser(); - // rendering issues on gantt sidebar - const GanttSidebarBlockView = ({ data }: any) => ( -
-
s.value === data.status)?.color, - }} - /> -
{data?.name}
-
- ); - const handleModuleUpdate = (module: IModule, payload: IBlockUpdateData) => { if (!workspaceSlug || !user) return; @@ -98,8 +85,8 @@ export const ModulesListGanttChartView: FC = ({ modules, mutateModules }) loaderTitle="Modules" blocks={modules ? blockFormat(modules) : null} blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)} - sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + SidebarBlockRender={ModuleGanttSidebarBlock} + BlockRender={ModuleGanttBlock} />
); diff --git a/apps/app/components/modules/index.ts b/apps/app/components/modules/index.ts index 5a5e2a4f147..2a7f54fb327 100644 --- a/apps/app/components/modules/index.ts +++ b/apps/app/components/modules/index.ts @@ -4,6 +4,5 @@ export * from "./delete-module-modal"; export * from "./form"; export * from "./gantt-chart"; export * from "./modal"; -export * from "./modules-list-gantt-chart"; export * from "./sidebar"; export * from "./single-module-card"; diff --git a/apps/app/components/views/gantt-chart.tsx b/apps/app/components/views/gantt-chart.tsx index 630ffaca0eb..36022f6fa4b 100644 --- a/apps/app/components/views/gantt-chart.tsx +++ b/apps/app/components/views/gantt-chart.tsx @@ -7,11 +7,8 @@ import useGanttChartViewIssues from "hooks/gantt-chart/view-issues-view"; import useUser from "hooks/use-user"; import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; // components -import { - GanttChartRoot, - IssueGanttBlock, - renderIssueBlocksStructure, -} from "components/gantt-chart"; +import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart"; +import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; // types import { IIssue } from "types"; @@ -29,28 +26,18 @@ export const ViewIssuesGanttChartView: FC = ({}) => { viewId as string ); - // rendering issues on gantt sidebar - const GanttSidebarBlockView = ({ data }: any) => ( -
-
-
{data?.name}
-
- ); - return ( -
+
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) } - sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + SidebarBlockRender={IssueGanttSidebarBlock} + BlockRender={IssueGanttBlock} />
); diff --git a/apps/app/constants/issue.ts b/apps/app/constants/issue.ts index 4e8bb0944a3..ff931707095 100644 --- a/apps/app/constants/issue.ts +++ b/apps/app/constants/issue.ts @@ -19,6 +19,7 @@ export const ORDER_BY_OPTIONS: Array<{ { name: "Manual", key: "sort_order" }, { name: "Last created", key: "-created_at" }, { name: "Last updated", key: "-updated_at" }, + { name: "Start date", key: "start_date" }, { name: "Priority", key: "priority" }, ]; diff --git a/apps/app/helpers/date-time.helper.ts b/apps/app/helpers/date-time.helper.ts index a98f08cb796..39a68bf3b32 100644 --- a/apps/app/helpers/date-time.helper.ts +++ b/apps/app/helpers/date-time.helper.ts @@ -374,3 +374,32 @@ export const getAllTimeIn30MinutesInterval = (): Array<{ { label: "11:00", value: "11:00" }, { label: "11:30", value: "11:30" }, ]; + +/** + * @returns {number} total number of days in range + * @description Returns total number of days in range + * @param {string} startDate + * @param {string} endDate + * @param {boolean} inclusive + * @example checkIfStringIsDate("2021-01-01", "2021-01-08") // 8 + */ + +export const findTotalDaysInRange = ( + startDate: Date | string, + endDate: Date | string, + inclusive: boolean +): number => { + if (!startDate || !endDate) return 0; + + startDate = new Date(startDate); + endDate = new Date(endDate); + + // find number of days between startDate and endDate + const diffInTime = endDate.getTime() - startDate.getTime(); + const diffInDays = diffInTime / (1000 * 3600 * 24); + + // if inclusive is true, add 1 to diffInDays + if (inclusive) return diffInDays + 1; + + return diffInDays; +}; diff --git a/apps/app/hooks/gantt-chart/cycle-issues-view.tsx b/apps/app/hooks/gantt-chart/cycle-issues-view.tsx index 25baf0d3e77..7ef534fb483 100644 --- a/apps/app/hooks/gantt-chart/cycle-issues-view.tsx +++ b/apps/app/hooks/gantt-chart/cycle-issues-view.tsx @@ -18,7 +18,13 @@ const useGanttChartCycleIssues = ( order_by: orderBy, type: filters?.type ? filters?.type : undefined, sub_issue: showSubIssues, - start_target_date: true, + assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, + state: filters?.state ? filters?.state.join(",") : undefined, + priority: filters?.priority ? filters?.priority.join(",") : undefined, + labels: filters?.labels ? filters?.labels.join(",") : undefined, + created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, + start_target_date: true, // to fetch only issues with a start and target date }; // all issues under the workspace and project diff --git a/apps/app/hooks/gantt-chart/issue-view.tsx b/apps/app/hooks/gantt-chart/issue-view.tsx index c7ffa0ffea5..7e595a35876 100644 --- a/apps/app/hooks/gantt-chart/issue-view.tsx +++ b/apps/app/hooks/gantt-chart/issue-view.tsx @@ -14,7 +14,13 @@ const useGanttChartIssues = (workspaceSlug: string | undefined, projectId: strin order_by: orderBy, type: filters?.type ? filters?.type : undefined, sub_issue: showSubIssues, - start_target_date: true, + assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, + state: filters?.state ? filters?.state.join(",") : undefined, + priority: filters?.priority ? filters?.priority.join(",") : undefined, + labels: filters?.labels ? filters?.labels.join(",") : undefined, + created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, + start_target_date: true, // to fetch only issues with a start and target date }; // all issues under the workspace and project diff --git a/apps/app/hooks/gantt-chart/module-issues-view.tsx b/apps/app/hooks/gantt-chart/module-issues-view.tsx index ca686f4e0ec..54dea3e2ee8 100644 --- a/apps/app/hooks/gantt-chart/module-issues-view.tsx +++ b/apps/app/hooks/gantt-chart/module-issues-view.tsx @@ -18,7 +18,13 @@ const useGanttChartModuleIssues = ( order_by: orderBy, type: filters?.type ? filters?.type : undefined, sub_issue: showSubIssues, - start_target_date: true, + assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, + state: filters?.state ? filters?.state.join(",") : undefined, + priority: filters?.priority ? filters?.priority.join(",") : undefined, + labels: filters?.labels ? filters?.labels.join(",") : undefined, + created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, + start_target_date: true, // to fetch only issues with a start and target date }; // all issues under the workspace and project diff --git a/apps/app/hooks/gantt-chart/view-issues-view.tsx b/apps/app/hooks/gantt-chart/view-issues-view.tsx index b66b3512806..8e0bc496b89 100644 --- a/apps/app/hooks/gantt-chart/view-issues-view.tsx +++ b/apps/app/hooks/gantt-chart/view-issues-view.tsx @@ -18,7 +18,7 @@ const useGanttChartViewIssues = ( // all issues under the view const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( workspaceSlug && projectId && viewId - ? VIEW_ISSUES(viewId.toString(), { ...viewGanttParams, start_target_date: true }) + ? VIEW_ISSUES(viewId.toString(), { ...viewGanttParams, order_by, start_target_date: true }) : null, workspaceSlug && projectId && viewId ? () => diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 98f678d5d81..44ccf42ae0a 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -18,10 +18,10 @@ import { SingleModuleCard, } from "components/modules"; // ui -import { EmptyState, Loader, PrimaryButton } from "components/ui"; +import { EmptyState, Icon, Loader, PrimaryButton, Tooltip } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons -import { PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +import { PlusIcon } from "@heroicons/react/24/outline"; // images import emptyModule from "public/empty-state/module.svg"; // types @@ -30,7 +30,18 @@ import type { NextPage } from "next"; // fetch-keys import { MODULE_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; // helper -import { truncateText } from "helpers/string.helper"; +import { replaceUnderscoreIfSnakeCase, truncateText } from "helpers/string.helper"; + +const moduleViewOptions: { type: "grid" | "gantt_chart"; icon: any }[] = [ + { + type: "gantt_chart", + icon: "view_timeline", + }, + { + type: "grid", + icon: "table_rows", + }, +]; const ProjectModules: NextPage = () => { const [selectedModule, setSelectedModule] = useState(); @@ -64,6 +75,7 @@ const ProjectModules: NextPage = () => { useEffect(() => { if (createUpdateModule) return; + const timer = setTimeout(() => { setSelectedModule(undefined); clearTimeout(timer); @@ -79,16 +91,42 @@ const ProjectModules: NextPage = () => { } right={ - { - const e = new KeyboardEvent("keydown", { key: "m" }); - document.dispatchEvent(e); - }} - > - - Add Module - +
+ {moduleViewOptions.map((option) => ( + {replaceUnderscoreIfSnakeCase(option.type)} View + } + position="bottom" + > + + + ))} + { + const e = new KeyboardEvent("keydown", { key: "m" }); + document.dispatchEvent(e); + }} + > + + Add Module + +
} > { /> {modules ? ( modules.length > 0 ? ( -
-
-

Modules

-
- - -
-
+ <> {modulesView === "grid" && ( -
+
{modules.map((module) => ( { {modulesView === "gantt_chart" && ( )} -
+ ) : (