From 13890b89f9578423abbc009a473b1249117e402b Mon Sep 17 00:00:00 2001 From: iyamokuma Date: Sat, 15 Aug 2026 13:59:26 -0700 Subject: [PATCH] feat: narrow Gantt peek panel and suppress open after drag Cap side-peek width at 24rem for a more manageable info panel, and prevent the peek from opening when finishing a Gantt drag or resize. Co-authored-by: Cursor --- .../blockResizables/use-gantt-resizable.ts | 25 ++++++++++++------- .../issues/issue-layouts/gantt/blocks.tsx | 13 +++++++--- .../components/issues/peek-overview/view.tsx | 2 +- .../store/timeline/base-timeline.store.ts | 11 ++++++++ 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/apps/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts b/apps/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts index 3a79ec618ae..c72c7420a1e 100644 --- a/apps/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts +++ b/apps/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts @@ -27,8 +27,8 @@ export const useGanttResizable = ( }); const ganttContainerDimensions = useRef(); const currMouseEvent = useRef(); - // states - const { currentViewData, updateBlockPosition, setIsDragging, getUpdatedPositionAfterDrag } = useTimeLineChartStore(); + const { currentViewData, updateBlockPosition, setIsDragging, setSuppressPeekOpen, getUpdatedPositionAfterDrag } = + useTimeLineChartStore(); const [isMoving, setIsMoving] = useState<"left" | "right" | "move" | undefined>(); // handle block resize from the left end @@ -36,6 +36,7 @@ export const useGanttResizable = ( e: React.MouseEvent, dragDirection: "left" | "right" | "move" ) => { + let hasMoved = false; const ganttContainerElement = ganttContainerRef.current; if (!currentViewData || !resizableRef.current || !block.position || !ganttContainerElement) return; @@ -59,8 +60,9 @@ export const useGanttResizable = ( if (currMouseEvent.current) handleMouseMove(currMouseEvent.current); }; - const handleMouseMove = (e: MouseEvent) => { - currMouseEvent.current = e; + const handleMouseMove = (mouseEvent: MouseEvent) => { + hasMoved = true; + currMouseEvent.current = mouseEvent; setIsMoving(dragDirection); setIsDragging(true); @@ -68,14 +70,14 @@ export const useGanttResizable = ( const { left: containerLeft } = ganttContainerDimensions.current; - const mouseX = e.clientX - containerLeft - SIDEBAR_WIDTH + ganttContainerElement.scrollLeft; + const currentMouseX = mouseEvent.clientX - containerLeft - SIDEBAR_WIDTH + ganttContainerElement.scrollLeft; let width = initialPositionRef.current.width; let marginLeft = 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; + marginLeft = Math.round(currentMouseX / dayWidth) * dayWidth; // get Dimensions from dom's style const prevMarginLeft = parseFloat(resizableDiv.style.marginLeft.slice(0, -2)); const prevWidth = parseFloat(resizableDiv.style.width.slice(0, -2)); @@ -85,18 +87,18 @@ export const useGanttResizable = ( width = block.target_date ? prevWidth + marginDelta : DEFAULT_BLOCK_WIDTH; } else if (dragDirection === "right") { // calculate new width and update the initialMarginLeft using += - width = Math.round(mouseX / dayWidth) * dayWidth - marginLeft; + width = Math.round(currentMouseX / dayWidth) * dayWidth - marginLeft; // If start date does not exist while dragging with right handle the revert to default width and adjust marginLeft accordingly if (!block.start_date) { // calculate new right and update the marginLeft to the newly calculated one - const marginRight = Math.round(mouseX / dayWidth) * dayWidth; + const marginRight = Math.round(currentMouseX / dayWidth) * dayWidth; marginLeft = marginRight - DEFAULT_BLOCK_WIDTH; width = DEFAULT_BLOCK_WIDTH; } } else if (dragDirection === "move") { // calculate new marginLeft and update the initial marginLeft using -= - marginLeft = Math.round((mouseX - initialPositionRef.current.offsetX) / dayWidth) * dayWidth; + marginLeft = Math.round((currentMouseX - initialPositionRef.current.offsetX) / dayWidth) * dayWidth; } // block needs to be at least 1 dayWidth Wide @@ -136,6 +138,11 @@ export const useGanttResizable = ( } setIsDragging(false); + + if (hasMoved) { + setSuppressPeekOpen(true); + window.setTimeout(() => setSuppressPeekOpen(false), 300); + } }; document.addEventListener("mousemove", handleMouseMove); diff --git a/apps/web/core/components/issues/issue-layouts/gantt/blocks.tsx b/apps/web/core/components/issues/issue-layouts/gantt/blocks.tsx index 57f1cebe58c..38c3022905c 100644 --- a/apps/web/core/components/issues/issue-layouts/gantt/blocks.tsx +++ b/apps/web/core/components/issues/issue-layouts/gantt/blocks.tsx @@ -10,7 +10,7 @@ import { useParams } from "next/navigation"; import { Popover } from "@plane/propel/popover"; import { Tooltip } from "@plane/propel/tooltip"; import { ControlLink } from "@plane/ui"; -import { findTotalDaysInRange, generateWorkItemLink } from "@plane/utils"; +import { generateWorkItemLink } from "@plane/utils"; // components import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants"; import { IssueIdentifier } from "@/components/issues/issue-detail/issue-identifier"; @@ -21,6 +21,7 @@ import { useProject } from "@/hooks/store/use-project"; import { useProjectState } from "@/hooks/store/use-project-state"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; +import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; import { usePlatformOS } from "@/hooks/use-platform-os"; // local imports import { WorkItemPreviewCard } from "../../preview-card"; @@ -45,6 +46,7 @@ export const IssueGanttBlock = observer(function IssueGanttBlock(props: Props) { // hooks const { isMobile } = usePlatformOS(); const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic); + const { suppressPeekOpen } = useTimeLineChartStore(); // derived values const issueDetails = getIssueById(issueId); @@ -53,9 +55,10 @@ export const IssueGanttBlock = observer(function IssueGanttBlock(props: Props) { const { blockStyle } = getBlockViewDetails(issueDetails, stateDetails?.color ?? ""); - const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile); - - const duration = findTotalDaysInRange(issueDetails?.start_date, issueDetails?.target_date) || 0; + const handleIssuePeekOverview = () => { + if (suppressPeekOpen) return; + handleRedirection(workspaceSlug, issueDetails, isMobile); + }; return ( @@ -113,6 +116,7 @@ export const IssueGanttSidebarBlock = observer(function IssueGanttSidebarBlock(p // handlers const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic); + const { suppressPeekOpen } = useTimeLineChartStore(); // derived values const issueDetails = getIssueById(issueId); @@ -121,6 +125,7 @@ export const IssueGanttSidebarBlock = observer(function IssueGanttSidebarBlock(p const handleIssuePeekOverview = (e: any) => { e.stopPropagation(true); e.preventDefault(); + if (suppressPeekOpen) return; handleRedirection(workspaceSlug, issueDetails, isMobile); }; diff --git a/apps/web/core/components/issues/peek-overview/view.tsx b/apps/web/core/components/issues/peek-overview/view.tsx index 333e4038c3d..c870898e3ca 100644 --- a/apps/web/core/components/issues/peek-overview/view.tsx +++ b/apps/web/core/components/issues/peek-overview/view.tsx @@ -123,7 +123,7 @@ export const IssueView = observer(function IssueView(props: IIssueView) { ? "absolute z-[25] flex flex-col overflow-hidden rounded-sm border border-subtle bg-surface-1 transition-all duration-300" : `h-full w-full`, !embedIssue && { - "top-0 right-0 bottom-0 w-full border-0 border-l md:w-[50%]": peekMode === "side-peek", + "top-0 right-0 bottom-0 w-full max-w-[24rem] border-0 border-l": peekMode === "side-peek", "top-[8.33%] left-[8.33%] size-5/6": peekMode === "modal", "absolute inset-0 m-4": peekMode === "full-screen", } diff --git a/apps/web/core/store/timeline/base-timeline.store.ts b/apps/web/core/store/timeline/base-timeline.store.ts index d5b1a55ecc5..9aab6b25bd0 100644 --- a/apps/web/core/store/timeline/base-timeline.store.ts +++ b/apps/web/core/store/timeline/base-timeline.store.ts @@ -43,6 +43,7 @@ export interface IBaseTimelineStore { activeBlockId: string | null; renderView: any; isDragging: boolean; + suppressPeekOpen: boolean; isDependencyEnabled: boolean; // setBlockIds: (ids: string[]) => void; @@ -64,6 +65,7 @@ export interface IBaseTimelineStore { updateBlockPosition: (id: string, deltaLeft: number, deltaWidth: number, ignoreDependencies?: boolean) => void; getNumberOfDaysFromPosition: (position: number | undefined) => number | undefined; setIsDragging: (isDragging: boolean) => void; + setSuppressPeekOpen: (suppressPeekOpen: boolean) => void; initGantt: () => void; getDateFromPositionOnGantt: (position: number, offsetDays: number) => Date | undefined; @@ -75,6 +77,7 @@ export class BaseTimeLineStore implements IBaseTimelineStore { blockIds: string[] | undefined = undefined; isDragging: boolean = false; + suppressPeekOpen: boolean = false; currentView: TGanttViews = "week"; currentViewData: ChartDataType | undefined = undefined; activeBlockId: string | null = null; @@ -90,12 +93,14 @@ export class BaseTimeLineStore implements IBaseTimelineStore { blocksMap: observable, blockIds: observable, isDragging: observable.ref, + suppressPeekOpen: observable.ref, currentView: observable.ref, currentViewData: observable, activeBlockId: observable.ref, renderView: observable, // actions setIsDragging: action, + setSuppressPeekOpen: action, setBlockIds: action.bound, initGantt: action.bound, updateCurrentView: action.bound, @@ -127,6 +132,12 @@ export class BaseTimeLineStore implements IBaseTimelineStore { }); }; + setSuppressPeekOpen = (suppressPeekOpen: boolean) => { + runInAction(() => { + this.suppressPeekOpen = suppressPeekOpen; + }); + }; + /** * @description check if block is active * @param {string} blockId