[WEB-2442] fix: Timeline Improvements and bug fixes (#5922)

* improve auto scroller logic

* fix drag indicator visibility on for blocks

* modify timeline store logic and improve timeline scrolling logic

* fix width of block while dragging with left handle

* fix block arrow direction while block is out of viewport
This commit is contained in:
rahulramesha
2024-10-29 13:42:14 +05:30
committed by GitHub
parent a88a39fb1e
commit 724adeff5c
15 changed files with 128 additions and 60 deletions

View File

@@ -9,7 +9,7 @@ import { getDateFromPositionOnGantt, getItemPositionWidth } from "@/components/g
// helpers
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// store
import { CoreRootStore } from "@/store/root.store";
import { RootStore } from "@/plane-web/store/root.store";
// types
type BlockData = {
@@ -38,6 +38,7 @@ export interface IBaseTimelineStore {
updateCurrentViewData: (data: ChartDataType | undefined) => void;
updateActiveBlockId: (blockId: string | null) => void;
updateRenderView: (data: any) => void;
updateAllBlocksOnChartChangeWhileDragging: (addedWidth: number) => void;
getUpdatedPositionAfterDrag: (id: string, ignoreDependencies?: boolean) => IBlockUpdateDependencyData[];
updateBlockPosition: (id: string, deltaLeft: number, deltaWidth: number, ignoreDependencies?: boolean) => void;
getNumberOfDaysFromPosition: (position: number | undefined) => number | undefined;
@@ -57,9 +58,9 @@ export class BaseTimeLineStore implements IBaseTimelineStore {
activeBlockId: string | null = null;
renderView: any = [];
rootStore: CoreRootStore;
rootStore: RootStore;
constructor(_rootStore: CoreRootStore) {
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
blocksMap: observable,
@@ -232,6 +233,24 @@ export class BaseTimeLineStore implements IBaseTimelineStore {
return getDateFromPositionOnGantt(position, this.currentViewData, offsetDays);
});
/**
* Adds width on Chart position change while the blocks are being dragged
* @param addedWidth
*/
updateAllBlocksOnChartChangeWhileDragging = action((addedWidth: number) => {
if (!this.blockIds || !this.isDragging) return;
runInAction(() => {
this.blockIds?.forEach((blockId) => {
const currBlock = this.blocksMap[blockId];
if (!currBlock || !currBlock.position) return;
currBlock.position.marginLeft += addedWidth;
});
});
});
/**
* returns updates dates of blocks post drag.
* @param id

View File

@@ -48,7 +48,7 @@ export const BlockRow: React.FC<Props> = observer((props) => {
(entries) => {
entries.forEach((entry) => {
setIsHidden(!entry.isIntersecting);
setIsBlockHiddenOnLeft(entry.boundingClientRect.left < 0);
setIsBlockHiddenOnLeft(entry.boundingClientRect.right < (entry.rootBounds?.left ?? 0));
});
},
{

View File

@@ -98,11 +98,11 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth;
const approxRangeLeft = scrollLeft;
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
if (approxRangeRight < 1000) updateCurrentViewRenderPayload("right", currentView);
if (approxRangeLeft < 1000) updateCurrentViewRenderPayload("left", currentView);
if (approxRangeRight < clientWidth) updateCurrentViewRenderPayload("right", currentView);
if (approxRangeLeft < clientWidth) updateCurrentViewRenderPayload("left", currentView);
};
const CHART_VIEW_COMPONENTS: {

View File

@@ -10,7 +10,15 @@ import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
import { SIDEBAR_WIDTH } from "../constants";
import { currentViewDataWithView } from "../data";
import { ChartDataType, IBlockUpdateData, IBlockUpdateDependencyData, TGanttViews } from "../types";
import { getNumberOfDaysBetweenTwoDates, IMonthBlock, IMonthView, IWeekBlock, timelineViewHelpers } from "../views";
import {
getNumberOfDaysBetweenTwoDates,
IMonthBlock,
IMonthView,
IWeekBlock,
monthView,
quarterView,
weekView,
} from "../views";
type ChartViewRootProps = {
border: boolean;
@@ -35,6 +43,12 @@ type ChartViewRootProps = {
showToday: boolean;
};
const timelineViewHelpers = {
week: weekView,
month: monthView,
quarter: quarterView,
};
export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
const {
border,
@@ -62,8 +76,15 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
const [fullScreenMode, setFullScreenMode] = useState(false);
// hooks
const { currentView, currentViewData, renderView, updateCurrentView, updateCurrentViewData, updateRenderView } =
useTimeLineChartStore();
const {
currentView,
currentViewData,
renderView,
updateCurrentView,
updateCurrentViewData,
updateRenderView,
updateAllBlocksOnChartChangeWhileDragging,
} = useTimeLineChartStore();
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
const selectedCurrentView: TGanttViews = view;
@@ -89,6 +110,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
updateCurrentView(selectedCurrentView);
updateRenderView(mergeRenderPayloads(currentRender.payload, renderView));
updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
updateAllBlocksOnChartChangeWhileDragging(currentRender.scrollWidth);
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else if (side === "right") {
updateCurrentView(view);

View File

@@ -90,7 +90,7 @@ export const VIEWS_LIST: ChartDataType[] = [
startDate: new Date(),
currentDate: new Date(),
endDate: new Date(),
approxFilterRange: 18, // it will preview week starting dates all months data and there is 3 months limitation for preview ex: title (2, 9, 16, 23, 30)
approxFilterRange: 24, // it will preview week starting dates all months data and there is 3 months limitation for preview ex: title (2, 9, 16, 23, 30)
dayWidth: 5,
},
},

View File

@@ -50,9 +50,9 @@ export const LeftResizable = observer((props: LeftResizableProps) => {
/>
<div
className={cn(
"absolute left-1 top-1/2 -translate-y-1/2 h-7 w-1 z-[5] rounded-sm bg-custom-background-100 transition-all duration-300",
"absolute left-1 top-1/2 -translate-y-1/2 h-7 w-1 z-[5] rounded-sm bg-custom-background-100 transition-all duration-300 opacity-0 group-hover:opacity-100",
{
"-left-1.5": isLeftResizing,
"-left-1.5 opacity-100": isLeftResizing,
}
)}
/>

View File

@@ -48,9 +48,9 @@ export const RightResizable = observer((props: RightResizableProps) => {
/>
<div
className={cn(
"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-1 z-[5] rounded-sm bg-custom-background-100 transition-all duration-300",
"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-1 z-[5] rounded-sm bg-custom-background-100 transition-all duration-300 opacity-0 group-hover:opacity-100",
{
"-right-1.5": isRightResizing,
"-right-1.5 opacity-100": isRightResizing,
}
)}
/>

View File

@@ -66,13 +66,16 @@ export const useGanttResizable = (
let width = initialPositionRef.current.width;
let marginLeft = initialPositionRef.current.marginLeft;
const blockRight = initialPositionRef.current.width + initialPositionRef.current.marginLeft;
if (dragDirection === "left") {
// 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 prevWidth = parseFloat(resizableDiv.style.width.slice(0, -2));
// calculate new width
width = blockRight - marginLeft;
const marginDelta = prevMarginLeft - marginLeft;
width = prevWidth + marginDelta;
} else if (dragDirection === "right") {
// calculate new width and update the initialMarginLeft using +=
width = Math.round(mouseX / dayWidth) * dayWidth - marginLeft;

View File

@@ -113,9 +113,3 @@ export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttB
return { marginLeft: scrollPosition, width: scrollWidth };
};
export const timelineViewHelpers = {
week: weekView,
month: monthView,
quarter: quarterView,
};

View File

@@ -3,7 +3,7 @@ import uniqBy from "lodash/uniqBy";
//
import { months } from "../data";
import { ChartDataType } from "../types";
import { getNumberOfDaysInMonth } from "./helpers";
import { getNumberOfDaysBetweenTwoDates, getNumberOfDaysInMonth } from "./helpers";
import { getWeeksBetweenTwoDates, IWeekBlock } from "./week-view";
export interface IMonthBlock {
@@ -38,6 +38,9 @@ const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "
let minusDate: Date = new Date();
let plusDate: Date = new Date();
let startDate = new Date();
let endDate = new Date();
// if side is null generate months on both side of current date
if (side === null) {
const currentDate = renderState.data.currentDate;
@@ -47,12 +50,14 @@ const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "
if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate);
startDate = filteredDates.weeks[0]?.startDate;
endDate = filteredDates.weeks[filteredDates.weeks.length - 1]?.endDate;
renderState = {
...renderState,
data: {
...renderState.data,
startDate: filteredDates.weeks[0]?.startDate,
endDate: filteredDates.weeks[filteredDates.weeks.length - 1]?.endDate,
startDate,
endDate,
},
};
}
@@ -65,9 +70,11 @@ const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "
if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate);
startDate = filteredDates.weeks[0]?.startDate;
endDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1);
renderState = {
...renderState,
data: { ...renderState.data, startDate: filteredDates.weeks[0].startDate },
data: { ...renderState.data, startDate },
};
}
// When side is right, generate more months on the right side of the end date
@@ -79,13 +86,16 @@ const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "
if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate);
startDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1);
endDate = filteredDates.weeks[filteredDates.weeks.length - 1]?.endDate;
renderState = {
...renderState,
data: { ...renderState.data, endDate: filteredDates.weeks[filteredDates.weeks.length - 1]?.endDate },
};
}
const scrollWidth = filteredDates.weeks.length * monthPayload.data.dayWidth * 7;
const days = Math.abs(getNumberOfDaysBetweenTwoDates(startDate, endDate)) + 1;
const scrollWidth = days * monthPayload.data.dayWidth;
return { state: renderState, payload: filteredDates, scrollWidth: scrollWidth };
};

View File

@@ -27,6 +27,9 @@ const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left"
let minusDate: Date = new Date();
let plusDate: Date = new Date();
let startDate = new Date();
let endDate = new Date();
// if side is null generate months on both side of current date
if (side === null) {
const currentDate = renderState.data.currentDate;
@@ -38,12 +41,15 @@ const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left"
const startMonthBlock = filteredDates[0];
const endMonthBlock = filteredDates[filteredDates.length - 1];
startDate = new Date(startMonthBlock.year, startMonthBlock.month, 1);
endDate = new Date(endMonthBlock.year, endMonthBlock.month + 1, 0);
renderState = {
...renderState,
data: {
...renderState.data,
startDate: new Date(startMonthBlock.year, startMonthBlock.month, 1),
endDate: new Date(endMonthBlock.year, endMonthBlock.month + 1, 0),
startDate,
endDate,
},
};
}
@@ -51,15 +57,17 @@ const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left"
else if (side === "left") {
const currentDate = renderState.data.startDate;
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range / 2, 1);
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1);
if (minusDate && plusDate) filteredDates = getMonthsBetweenTwoDates(minusDate, plusDate);
const startMonthBlock = filteredDates[0];
startDate = new Date(startMonthBlock.year, startMonthBlock.month, 1);
endDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1);
renderState = {
...renderState,
data: { ...renderState.data, startDate: new Date(startMonthBlock.year, startMonthBlock.month, 1) },
data: { ...renderState.data, startDate },
};
}
// When side is right, generate more months on the right side of the end date
@@ -67,24 +75,20 @@ const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left"
const currentDate = renderState.data.endDate;
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 1);
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range / 2, 1);
if (minusDate && plusDate) filteredDates = getMonthsBetweenTwoDates(minusDate, plusDate);
const endMonthBlock = filteredDates[filteredDates.length - 1];
startDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1);
endDate = new Date(endMonthBlock.year, endMonthBlock.month + 1, 0);
renderState = {
...renderState,
data: { ...renderState.data, endDate: new Date(endMonthBlock.year, endMonthBlock.month + 1, 0) },
data: { ...renderState.data, endDate },
};
}
const startMonthBlock = filteredDates[0];
const endMonthBlock = filteredDates[filteredDates.length - 1];
const startDate = new Date(startMonthBlock.year, startMonthBlock.month, 1);
const endDate = new Date(endMonthBlock.year, endMonthBlock.month + 1, 0);
const days = Math.abs(getNumberOfDaysBetweenTwoDates(startDate, endDate));
const days = Math.abs(getNumberOfDaysBetweenTwoDates(startDate, endDate)) + 1;
const scrollWidth = days * quarterPayload.data.dayWidth;
return { state: renderState, payload: filteredDates, scrollWidth: scrollWidth };

View File

@@ -1,7 +1,7 @@
//
import { weeks, months } from "../data";
import { ChartDataType } from "../types";
import { getWeekNumberByDate } from "./helpers";
import { getNumberOfDaysBetweenTwoDates, getWeekNumberByDate } from "./helpers";
export interface IDayBlock {
date: Date;
day: number;
@@ -46,6 +46,9 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
let minusDate: Date = new Date();
let plusDate: Date = new Date();
let startDate = new Date();
let endDate = new Date();
// if side is null generate weeks on both side of current date
if (side === null) {
const currentDate = renderState.data.currentDate;
@@ -55,12 +58,14 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
startDate = filteredDates[0].startDate;
endDate = filteredDates[filteredDates.length - 1].endDate;
renderState = {
...renderState,
data: {
...renderState.data,
startDate: filteredDates[0].startDate,
endDate: filteredDates[filteredDates.length - 1].endDate,
startDate,
endDate,
},
};
}
@@ -73,9 +78,11 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
startDate = filteredDates[0].startDate;
endDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1);
renderState = {
...renderState,
data: { ...renderState.data, startDate: filteredDates[0].startDate },
data: { ...renderState.data, startDate },
};
}
// When side is right, generate more weeks on the right side of the end date
@@ -87,16 +94,16 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
startDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1);
endDate = filteredDates[filteredDates.length - 1].endDate;
renderState = {
...renderState,
data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1].endDate },
data: { ...renderState.data, endDate },
};
}
const scrollWidth =
filteredDates
.map((monthData: any) => monthData.children.length)
.reduce((partialSum: number, a: number) => partialSum + a, 0) * weekPayload.data.dayWidth;
const days = Math.abs(getNumberOfDaysBetweenTwoDates(startDate, endDate)) + 1;
const scrollWidth = days * weekPayload.data.dayWidth;
return { state: renderState, payload: filteredDates, scrollWidth: scrollWidth };
};

View File

@@ -1,6 +1,6 @@
import { RefObject, useEffect, useRef } from "react";
const SCROLL_BY = 1;
const SCROLL_BY = 3;
const AUTO_SCROLL_THRESHOLD = 15;
const MAX_SPEED_THRESHOLD = 5;
@@ -13,6 +13,7 @@ export const useAutoScroller = (
) => {
const containerDimensions = useRef<DOMRect | undefined>();
const intervalId = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
const mousePosition = useRef<{ clientX: number; clientY: number } | undefined>(undefined);
const clearRegisteredTimeout = () => {
clearInterval(intervalId.current);
@@ -26,6 +27,15 @@ export const useAutoScroller = (
if (!rect || !shouldScroll || (e.clientX === 0 && e.clientY === 0)) return;
let diffX = 0,
diffY = 0;
if (mousePosition.current) {
diffX = e.clientX - mousePosition.current.clientX;
diffY = e.clientY - mousePosition.current.clientY;
}
mousePosition.current = { clientX: e.clientX, clientY: e.clientY };
const { left, top, width, height } = rect;
const mouseX = e.clientX - left - leftOffset;
@@ -44,28 +54,28 @@ export const useAutoScroller = (
scrollByY = 0;
// Check mouse positions against thresholds
if (mouseX < thresholdX) {
if (mouseX < thresholdX && diffX <= 0) {
scrollByX = -1 * SCROLL_BY;
if (mouseX < maxSpeedX) {
scrollByX *= 2;
}
}
if (mouseX > currWidth - thresholdX) {
if (mouseX > currWidth - thresholdX && diffX >= 0) {
scrollByX = SCROLL_BY;
if (mouseX > currWidth - maxSpeedX) {
scrollByX *= 2;
}
}
if (mouseY < thresholdY) {
if (mouseY < thresholdY && diffY <= 0) {
scrollByY = -1 * SCROLL_BY;
if (mouseX < maxSpeedY) {
scrollByY *= 2;
}
}
if (mouseY > currHeight - thresholdY) {
if (mouseY > currHeight - thresholdY && diffY >= 0) {
scrollByY = SCROLL_BY;
if (mouseY > currHeight - maxSpeedY) {
scrollByY *= 2;

View File

@@ -1,8 +1,7 @@
import { autorun } from "mobx";
// Plane-web
import { RootStore } from "@/plane-web/store/root.store";
import { BaseTimeLineStore, IBaseTimelineStore } from "@/plane-web/store/timeline/base-timeline.store";
// Store
import { CoreRootStore } from "@/store/root.store";
export interface IIssuesTimeLineStore extends IBaseTimelineStore {
isDependencyEnabled: boolean;
@@ -11,7 +10,7 @@ export interface IIssuesTimeLineStore extends IBaseTimelineStore {
export class IssuesTimeLineStore extends BaseTimeLineStore implements IIssuesTimeLineStore {
isDependencyEnabled = true;
constructor(_rootStore: CoreRootStore) {
constructor(_rootStore: RootStore) {
super(_rootStore);
autorun((reaction) => {

View File

@@ -1,6 +1,6 @@
import { autorun } from "mobx";
// Store
import { CoreRootStore } from "@/store/root.store";
import { RootStore } from "@/plane-web/store/root.store";
import { BaseTimeLineStore, IBaseTimelineStore } from "ce/store/timeline/base-timeline.store";
export interface IModulesTimeLineStore extends IBaseTimelineStore {
@@ -10,7 +10,7 @@ export interface IModulesTimeLineStore extends IBaseTimelineStore {
export class ModulesTimeLineStore extends BaseTimeLineStore implements IModulesTimeLineStore {
isDependencyEnabled = false;
constructor(_rootStore: CoreRootStore) {
constructor(_rootStore: RootStore) {
super(_rootStore);
autorun((reaction) => {