Skip to content

Commit

Permalink
[WEB-2442] feat: Minor Timeline view Enhancements (#5987)
Browse files Browse the repository at this point in the history
* fix timeline scroll to the right in some cases

(cherry picked from commit 17043a6)

* add get position based on Date

(cherry picked from commit 2fbe22d)

* Add sticky block name to enable it to be read throughout the block regardless of scroll position

(cherry picked from commit 447af2e)

* Enable blocks to have a single date on the block charts

(cherry picked from commit cb055d5)

* revert back date-range changes

* change gradient of half blocks on Timeline

* Add instance Id for Timeline Sidebar dragging to avoid enabling dropping of other drag instances

* fix timeline scrolling height
  • Loading branch information
rahulramesha authored Nov 13, 2024
1 parent f44db89 commit 4b50b27
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 90 deletions.
18 changes: 16 additions & 2 deletions web/ce/store/timeline/base-timeline.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
*/
Expand Down
2 changes: 1 addition & 1 deletion web/core/components/gantt-chart/blocks/block-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const BlockRow: React.FC<Props> = 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);
Expand Down
11 changes: 6 additions & 5 deletions web/core/components/gantt-chart/blocks/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ export const GanttChartBlock: React.FC<Props> = 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;

Expand All @@ -63,7 +64,7 @@ export const GanttChartBlock: React.FC<Props> = 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`,
}}
>
Expand All @@ -88,7 +89,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
handleBlockDrag={handleBlockDrag}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
enableBlockMove={enableBlockMove && !!isBlockComplete}
isMoving={isMoving}
ganttContainerRef={ganttContainerRef}
/>
Expand Down
18 changes: 13 additions & 5 deletions web/core/components/gantt-chart/chart/main-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -108,14 +108,20 @@ export const GanttChartMainContent: React.FC<Props> = 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;
Expand All @@ -129,7 +135,8 @@ export const GanttChartMainContent: React.FC<Props> = 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);
});
};

Expand Down Expand Up @@ -189,6 +196,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
style={{
width: `${itemsContainerWidth}px`,
transform: `translateY(${HEADER_HEIGHT}px)`,
paddingBottom: `${HEADER_HEIGHT}px`,
}}
>
<GanttChartRowList
Expand Down
2 changes: 1 addition & 1 deletion web/core/components/gantt-chart/chart/views/month.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const MonthChartView: FC<any> = observer(() => {
const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate);

return (
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
{currentViewData && (
<div className="relative flex flex-col outline-[0.25px] outline outline-custom-border-200">
{/** Header Div */}
Expand Down
2 changes: 1 addition & 1 deletion web/core/components/gantt-chart/chart/views/quarter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const QuarterChartView: FC<any> = observer(() => {
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);

return (
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
{currentViewData &&
quarterBlocks?.map((quarterBlock, rootIndex) => (
<div
Expand Down
2 changes: 1 addition & 1 deletion web/core/components/gantt-chart/chart/views/week.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const WeekChartView: FC<any> = observer(() => {
const weekBlocks: IWeekBlock[] = renderView;

return (
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
{currentViewData &&
weekBlocks?.map((block, rootIndex) => (
<div
Expand Down
2 changes: 2 additions & 0 deletions web/core/components/gantt-chart/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ export const GANTT_BREADCRUMBS_HEIGHT = 40;

export const SIDEBAR_WIDTH = 360;

export const DEFAULT_BLOCK_WIDTH = 60;

export const GANTT_SELECT_GROUP = "gantt-issues";
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const useGanttResizable = (
// calculate new marginLeft and update the initial marginLeft to the newly calculated one
marginLeft = Math.round(mouseX / dayWidth) * dayWidth;
// get Dimensions from dom's style
const prevMarginLeft = parseFloat(resizableDiv.style.transform.slice(11, -3));
const prevMarginLeft = parseFloat(resizableDiv.style.marginLeft.slice(0, -2));
const prevWidth = parseFloat(resizableDiv.style.width.slice(0, -2));
// calculate new width
const marginDelta = prevMarginLeft - marginLeft;
Expand All @@ -88,7 +88,7 @@ export const useGanttResizable = (
if (width < dayWidth) return;

resizableDiv.style.width = `${width}px`;
resizableDiv.style.transform = `translateX(${marginLeft}px)`;
resizableDiv.style.marginLeft = `${marginLeft}px`;

const deltaLeft = Math.round((marginLeft - (block.position?.marginLeft ?? 0)) / dayWidth) * dayWidth;
const deltaWidth = Math.round((width - (block.position?.width ?? 0)) / dayWidth) * dayWidth;
Expand Down
4 changes: 2 additions & 2 deletions web/core/components/gantt-chart/sidebar/gantt-dnd-HOC.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const GanttDnDHOC = observer((props: Props) => {
draggable({
element,
canDrag: () => isDragEnabled,
getInitialData: () => ({ id }),
getInitialData: () => ({ id, dragInstanceId: "GANTT_REORDER" }),
onDragStart: () => {
setIsDragging(true);
},
Expand All @@ -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 };

Expand Down
4 changes: 2 additions & 2 deletions web/core/components/gantt-chart/sidebar/issues/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions web/core/components/gantt-chart/sidebar/modules/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export const ModulesSidebarBlock: React.FC<Props> = 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 (
<div
Expand Down
50 changes: 33 additions & 17 deletions web/core/components/gantt-chart/views/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { addDaysToDate, findTotalDaysInRange, getDate } from "@/helpers/date-time.helper";
import { DEFAULT_BLOCK_WIDTH } from "../constants";
import { ChartDataType, IGanttBlock } from "../types";
import { IMonthBlock, IMonthView, monthView } from "./month-view";
import { quarterView } from "./quarter-view";
import { IWeekBlock, weekView } from "./week-view";

/**
* Generates Date by using Day, month and Year
Expand Down Expand Up @@ -84,32 +82,50 @@ export const getDateFromPositionOnGantt = (position: number, chartData: ChartDat
*/
export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttBlock) => {
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;

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;
};
58 changes: 33 additions & 25 deletions web/core/components/issues/issue-layouts/gantt/blocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {
Expand All @@ -39,36 +42,37 @@ export const IssueGanttBlock: React.FC<Props> = 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 (
<div
id={`issue-${issueId}`}
className="relative flex h-full w-full cursor-pointer items-center rounded"
style={{
backgroundColor: stateDetails?.color,
}}
onClick={handleIssuePeekOverview}
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{issueDetails?.name}</h5>
<div>{message}</div>
</div>
}
position="top-left"
disabled={!message}
>
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{issueDetails?.name}</h5>
<div>
{renderFormattedDate(issueDetails?.start_date ?? "")} to{" "}
{renderFormattedDate(issueDetails?.target_date ?? "")}
</div>
</div>
}
position="top-left"
<div
id={`issue-${issueId}`}
className="relative flex h-full w-full cursor-pointer items-center rounded"
style={blockStyle}
onClick={handleIssuePeekOverview}
>
<div className="relative w-full overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100">
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<div
className="sticky w-auto overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100"
style={{ left: `${SIDEBAR_WIDTH}px` }}
>
{issueDetails?.name}
</div>
</Tooltip>
</div>
</div>
</Tooltip>
);
});

Expand All @@ -92,7 +96,11 @@ export const IssueGanttSidebarBlock: React.FC<Props> = 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 (
<ControlLink
Expand Down
Loading

0 comments on commit 4b50b27

Please sign in to comment.