diff --git a/web/ce/store/timeline/base-timeline.store.ts b/web/ce/store/timeline/base-timeline.store.ts index c563297d8d9..71169ddadae 100644 --- a/web/ce/store/timeline/base-timeline.store.ts +++ b/web/ce/store/timeline/base-timeline.store.ts @@ -5,7 +5,11 @@ import { computedFn } from "mobx-utils"; // components import { ChartDataType, IBlockUpdateDependencyData, IGanttBlock, TGanttViews } from "@/components/gantt-chart"; import { currentViewDataWithView } from "@/components/gantt-chart/data"; -import { getDateFromPositionOnGantt, getItemPositionWidth } from "@/components/gantt-chart/views/helpers"; +import { + getDateFromPositionOnGantt, + getItemPositionWidth, + getPositionFromDate, +} from "@/components/gantt-chart/views/helpers"; // helpers import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; // store @@ -47,6 +51,7 @@ export interface IBaseTimelineStore { initGantt: () => void; getDateFromPositionOnGantt: (position: number, offsetDays: number) => Date | undefined; + getPositionFromDateOnGantt: (date: string | Date, offSetWidth: number) => number | undefined; } export class BaseTimeLineStore implements IBaseTimelineStore { @@ -186,7 +191,7 @@ export class BaseTimeLineStore implements IBaseTimelineStore { start_date: blockData?.start_date ?? undefined, target_date: blockData?.target_date ?? undefined, }; - if (this.currentViewData && this.currentViewData?.data?.startDate && this.currentViewData?.data?.dayWidth) { + if (this.currentViewData && (this.currentViewData?.data?.startDate || this.currentViewData?.data?.dayWidth)) { block.position = getItemPositionWidth(this.currentViewData, block); } @@ -227,6 +232,15 @@ export class BaseTimeLineStore implements IBaseTimelineStore { return Math.round(position / this.currentViewData.data.dayWidth); }; + /** + * returns position of the date on chart + */ + getPositionFromDateOnGantt = computedFn((date: string | Date, offSetWidth: number) => { + if (!this.currentViewData) return; + + return getPositionFromDate(this.currentViewData, date, offSetWidth); + }); + /** * returns the date at which the position corresponds to on the timeline chart */ diff --git a/web/core/components/gantt-chart/blocks/block-row.tsx b/web/core/components/gantt-chart/blocks/block-row.tsx index 1bcec7f389a..b215aee4b85 100644 --- a/web/core/components/gantt-chart/blocks/block-row.tsx +++ b/web/core/components/gantt-chart/blocks/block-row.tsx @@ -68,7 +68,7 @@ export const BlockRow: React.FC = observer((props) => { // hide the block if it doesn't have start and target dates and showAllBlocks is false if (!block || !block.data || (!showAllBlocks && !(block.start_date && block.target_date))) return null; - const isBlockVisibleOnChart = block.start_date && block.target_date; + const isBlockVisibleOnChart = block.start_date || block.target_date; const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id); const isBlockFocused = selectionHelpers.getIsEntityActive(block.id); const isBlockHoveredOn = isBlockActive(block.id); diff --git a/web/core/components/gantt-chart/blocks/block.tsx b/web/core/components/gantt-chart/blocks/block.tsx index a1c83c0dae4..8671993c7c9 100644 --- a/web/core/components/gantt-chart/blocks/block.tsx +++ b/web/core/components/gantt-chart/blocks/block.tsx @@ -46,10 +46,11 @@ export const GanttChartBlock: React.FC = observer((props) => { const { isMoving, handleBlockDrag } = useGanttResizable(block, resizableRef, ganttContainerRef, updateBlockDates); - // 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 isBlockComplete = block?.start_date && block?.target_date; - const isBlockVisibleOnChart = block.start_date && block.target_date; + // hide the block if it doesn't have start and target dates and showAllBlocks is false + if (!block || (!showAllBlocks && !isBlockVisibleOnChart)) return null; if (!block.data) return null; @@ -63,7 +64,7 @@ export const GanttChartBlock: React.FC = observer((props) => { ref={resizableRef} style={{ height: `${BLOCK_HEIGHT}px`, - transform: `translateX(${block.position?.marginLeft}px)`, + marginLeft: `${block.position?.marginLeft}px`, width: `${block.position?.width}px`, }} > @@ -88,7 +89,7 @@ export const GanttChartBlock: React.FC = observer((props) => { handleBlockDrag={handleBlockDrag} enableBlockLeftResize={enableBlockLeftResize} enableBlockRightResize={enableBlockRightResize} - enableBlockMove={enableBlockMove} + enableBlockMove={enableBlockMove && !!isBlockComplete} isMoving={isMoving} ganttContainerRef={ganttContainerRef} /> diff --git a/web/core/components/gantt-chart/chart/main-content.tsx b/web/core/components/gantt-chart/chart/main-content.tsx index 1c48622791b..b9582d21cee 100644 --- a/web/core/components/gantt-chart/chart/main-content.tsx +++ b/web/core/components/gantt-chart/chart/main-content.tsx @@ -28,7 +28,7 @@ import { IssueBulkOperationsRoot } from "@/plane-web/components/issues"; import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status"; // import { GanttChartRowList } from "../blocks/block-row-list"; -import { GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants"; +import { DEFAULT_BLOCK_WIDTH, GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants"; import { getItemPositionWidth } from "../views"; import { TimelineDragHelper } from "./timeline-drag-helper"; @@ -108,14 +108,20 @@ export const GanttChartMainContent: React.FC = observer((props) => { const approxRangeLeft = scrollLeft; const approxRangeRight = scrollWidth - (scrollLeft + clientWidth); + const calculatedRangeRight = itemsContainerWidth - (scrollLeft + clientWidth); - if (approxRangeRight < clientWidth) updateCurrentViewRenderPayload("right", currentView); - if (approxRangeLeft < clientWidth) updateCurrentViewRenderPayload("left", currentView); + if (approxRangeRight < clientWidth || calculatedRangeRight < clientWidth) { + updateCurrentViewRenderPayload("right", currentView); + } + if (approxRangeLeft < clientWidth) { + updateCurrentViewRenderPayload("left", currentView); + } }; const handleScrollToBlock = (block: IGanttBlock) => { const scrollContainer = ganttContainerRef.current as HTMLDivElement; - const scrollToDate = getDate(block.start_date); + const scrollToEndDate = !block.start_date && block.target_date; + const scrollToDate = block.start_date ? getDate(block.start_date) : getDate(block.target_date); let chartData; if (!scrollContainer || !currentViewData || !scrollToDate) return; @@ -129,7 +135,8 @@ export const GanttChartMainContent: React.FC = observer((props) => { const updatedPosition = getItemPositionWidth(chartData ?? currentViewData, block); setTimeout(() => { - if (updatedPosition) scrollContainer.scrollLeft = updatedPosition.marginLeft - 4; + if (updatedPosition) + scrollContainer.scrollLeft = updatedPosition.marginLeft - 4 - (scrollToEndDate ? DEFAULT_BLOCK_WIDTH : 0); }); }; @@ -189,6 +196,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { style={{ width: `${itemsContainerWidth}px`, transform: `translateY(${HEADER_HEIGHT}px)`, + paddingBottom: `${HEADER_HEIGHT}px`, }} > = observer(() => { const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate); return ( -
+
{currentViewData && (
{/** Header Div */} diff --git a/web/core/components/gantt-chart/chart/views/quarter.tsx b/web/core/components/gantt-chart/chart/views/quarter.tsx index 6252081e332..421d0bdfec6 100644 --- a/web/core/components/gantt-chart/chart/views/quarter.tsx +++ b/web/core/components/gantt-chart/chart/views/quarter.tsx @@ -15,7 +15,7 @@ export const QuarterChartView: FC = observer(() => { const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks); return ( -
+
{currentViewData && quarterBlocks?.map((quarterBlock, rootIndex) => (
= observer(() => { const weekBlocks: IWeekBlock[] = renderView; return ( -
+
{currentViewData && weekBlocks?.map((block, rootIndex) => (
{ draggable({ element, canDrag: () => isDragEnabled, - getInitialData: () => ({ id }), + getInitialData: () => ({ id, dragInstanceId: "GANTT_REORDER" }), onDragStart: () => { setIsDragging(true); }, @@ -44,7 +44,7 @@ export const GanttDnDHOC = observer((props: Props) => { }), dropTargetForElements({ element, - canDrop: ({ source }) => source?.data?.id !== id, + canDrop: ({ source }) => source?.data?.id !== id && source?.data?.dragInstanceId === "GANTT_REORDER", getData: ({ input, element }) => { const data = { id }; diff --git a/web/core/components/gantt-chart/sidebar/issues/block.tsx b/web/core/components/gantt-chart/sidebar/issues/block.tsx index 9b0dc270b24..bb286c28006 100644 --- a/web/core/components/gantt-chart/sidebar/issues/block.tsx +++ b/web/core/components/gantt-chart/sidebar/issues/block.tsx @@ -27,8 +27,8 @@ export const IssuesSidebarBlock = observer((props: Props) => { const { updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore(); const { getIsIssuePeeked } = useIssueDetail(); - const isBlockVisibleOnChart = !!block?.start_date && !!block?.target_date; - const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined; + const isBlockComplete = !!block?.start_date && !!block?.target_date; + const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined; if (!block?.data) return null; diff --git a/web/core/components/gantt-chart/sidebar/modules/block.tsx b/web/core/components/gantt-chart/sidebar/modules/block.tsx index 2dc85ac20a2..fb8eaf48183 100644 --- a/web/core/components/gantt-chart/sidebar/modules/block.tsx +++ b/web/core/components/gantt-chart/sidebar/modules/block.tsx @@ -22,8 +22,8 @@ export const ModulesSidebarBlock: React.FC = observer((props) => { if (!block) return <>; - const isBlockVisibleOnChart = !!block.start_date && !!block.target_date; - const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined; + const isBlockComplete = !!block.start_date && !!block.target_date; + const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined; return (
{ let scrollPosition: number = 0; - let scrollWidth: number = 0; + let scrollWidth: number = DEFAULT_BLOCK_WIDTH; const { startDate: chartStartDate } = chartData.data; const { start_date, target_date } = itemData; @@ -92,24 +90,42 @@ export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttB const itemStartDate = getDate(start_date); const itemTargetDate = getDate(target_date); - if (!itemStartDate || !itemTargetDate) return; - chartStartDate.setHours(0, 0, 0, 0); - itemStartDate.setHours(0, 0, 0, 0); - itemTargetDate.setHours(0, 0, 0, 0); - - // get number of days from chart start date to block's start date - const positionDaysDifference = Math.round(findTotalDaysInRange(chartStartDate, itemStartDate, false) ?? 0); + itemStartDate?.setHours(0, 0, 0, 0); + itemTargetDate?.setHours(0, 0, 0, 0); - if (!positionDaysDifference) return; + if (!itemStartDate && !itemTargetDate) return; // get scroll position from the number of days and width of each day - scrollPosition = positionDaysDifference * chartData.data.dayWidth; + scrollPosition = itemStartDate + ? getPositionFromDate(chartData, itemStartDate, 0) + : getPositionFromDate(chartData, itemTargetDate!, -1 * DEFAULT_BLOCK_WIDTH); - // get width of block - const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime(); - const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24))); - scrollWidth = (widthDaysDifference + 1) * chartData.data.dayWidth; + if (itemStartDate && itemTargetDate) { + // get width of block + const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime(); + const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24))); + scrollWidth = (widthDaysDifference + 1) * chartData.data.dayWidth; + } return { marginLeft: scrollPosition, width: scrollWidth }; }; + +export const getPositionFromDate = (chartData: ChartDataType, date: string | Date, offsetWidth: number) => { + const currDate = getDate(date); + + const { startDate: chartStartDate } = chartData.data; + + if (!currDate || !chartStartDate) return 0; + + chartStartDate.setHours(0, 0, 0, 0); + currDate.setHours(0, 0, 0, 0); + + // get number of days from chart start date to block's start date + const positionDaysDifference = Math.round(findTotalDaysInRange(chartStartDate, currDate, false) ?? 0); + + if (!positionDaysDifference) return 0; + + // get scroll position from the number of days and width of each day + return positionDaysDifference * chartData.data.dayWidth + offsetWidth; +}; diff --git a/web/core/components/issues/issue-layouts/gantt/blocks.tsx b/web/core/components/issues/issue-layouts/gantt/blocks.tsx index 5ffd2b3815a..efb2b6bd14c 100644 --- a/web/core/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/core/components/issues/issue-layouts/gantt/blocks.tsx @@ -4,6 +4,8 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // ui import { Tooltip, ControlLink } from "@plane/ui"; +// components +import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; // hooks @@ -13,7 +15,8 @@ import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-red import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { IssueIdentifier } from "@/plane-web/components/issues"; -// local types +// +import { getBlockViewDetails } from "../utils"; import { GanttStoreType } from "./base-gantt-root"; type Props = { @@ -39,36 +42,37 @@ export const IssueGanttBlock: React.FC = observer((props) => { const stateDetails = issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id); + const { message, blockStyle } = getBlockViewDetails(issueDetails, stateDetails?.color ?? ""); + const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile); return ( -
+
{issueDetails?.name}
+
{message}
+
+ } + position="top-left" + disabled={!message} > -
- -
{issueDetails?.name}
-
- {renderFormattedDate(issueDetails?.start_date ?? "")} to{" "} - {renderFormattedDate(issueDetails?.target_date ?? "")} -
-
- } - position="top-left" +
-
+
+
{issueDetails?.name}
- -
+
+ ); }); @@ -92,7 +96,11 @@ export const IssueGanttSidebarBlock: React.FC = observer((props) => { // derived values const issueDetails = getIssueById(issueId); - const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile); + const handleIssuePeekOverview = (e: any) => { + e.stopPropagation(true); + e.preventDefault(); + handleRedirection(workspaceSlug, issueDetails, isMobile); + }; return ( { + const isBlockVisibleOnChart = block?.start_date || block?.target_date; + const isBlockComplete = block?.start_date && block?.target_date; + + let message; + const blockStyle: CSSProperties = { + backgroundColor, + }; + + if (isBlockVisibleOnChart && !isBlockComplete) { + if (block?.start_date) { + message = `From ${renderFormattedDate(block.start_date)}`; + blockStyle.maskImage = `linear-gradient(to right, ${backgroundColor} 50%, transparent 95%)`; + } else if (block?.target_date) { + message = `Till ${renderFormattedDate(block.target_date)}`; + blockStyle.maskImage = `linear-gradient(to left, ${backgroundColor} 50%, transparent 95%)`; + } + } else if (isBlockComplete) { + message = `${renderFormattedDate(block?.start_date)} to ${renderFormattedDate(block?.target_date)}`; + } + + return { + message, + blockStyle, + }; +}; diff --git a/web/core/components/modules/gantt-chart/blocks.tsx b/web/core/components/modules/gantt-chart/blocks.tsx index 946b31398c7..8c1567011d8 100644 --- a/web/core/components/modules/gantt-chart/blocks.tsx +++ b/web/core/components/modules/gantt-chart/blocks.tsx @@ -3,13 +3,14 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useParams } from "next/navigation"; -// hooks // ui import { Tooltip, ModuleStatusIcon } from "@plane/ui"; -// helpers -import { MODULE_STATUS } from "@/constants/module"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; +// components +import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants"; +import { getBlockViewDetails } from "@/components/issues/issue-layouts/utils"; // constants +import { MODULE_STATUS } from "@/constants/module"; +// hooks import { useModule } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -30,31 +31,40 @@ export const ModuleGanttBlock: React.FC = observer((props) => { // hooks const { isMobile } = usePlatformOS(); + const { message, blockStyle } = getBlockViewDetails( + moduleDetails, + MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color ?? "" + ); + return ( -
s.value === moduleDetails?.status)?.color }} - onClick={() => - router.push(`/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`) + +
{moduleDetails?.name}
+
{message}
+
} + position="top-left" > -
- -
{moduleDetails?.name}
-
- {renderFormattedDate(moduleDetails?.start_date ?? "")} to{" "} - {renderFormattedDate(moduleDetails?.target_date ?? "")} -
-
+
+ router.push( + `/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}` + ) } - position="top-left" > -
{moduleDetails?.name}
- -
+
+
+ {moduleDetails?.name} +
+
+ ); });