snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useEffect, useRef, useState } from "react";
import { addDays } from "date-fns";
import { observer } from "mobx-react";
import { AddOutline } from "@makeplane/propel/icons";
// ui
import { Tooltip } from "@makeplane/propel/components/tooltip";
import type { IBlockUpdateData, IGanttBlock } from "@plane/types";
// helpers
import { renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
//
type Props = {
block: IGanttBlock;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
};
export const ChartAddBlock = observer(function ChartAddBlock(props: Props) {
const { block, blockUpdateHandler } = props;
// states
const [isButtonVisible, setIsButtonVisible] = useState(false);
const [buttonXPosition, setButtonXPosition] = useState(0);
const [buttonStartDate, setButtonStartDate] = useState<Date | null>(null);
// refs
const containerRef = useRef<HTMLDivElement>(null);
// hooks
const { isMobile } = usePlatformOS();
// chart hook
const { currentViewData, currentView } = useTimeLineChartStore();
const handleButtonClick = () => {
if (!currentViewData) return;
const { startDate: chartStartDate, dayWidth } = currentViewData.data;
const columnNumber = buttonXPosition / dayWidth;
let numberOfDays = 1;
if (currentView === "quarter") numberOfDays = 7;
const startDate = addDays(chartStartDate, columnNumber);
const endDate = addDays(startDate, numberOfDays);
blockUpdateHandler(block.data, {
start_date: renderFormattedPayloadDate(startDate) ?? undefined,
target_date: renderFormattedPayloadDate(endDate) ?? undefined,
meta: block.meta,
});
};
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleMouseMove = (e: MouseEvent) => {
if (!currentViewData) return;
setButtonXPosition(e.offsetX);
const { startDate: chartStartDate, dayWidth } = currentViewData.data;
const columnNumber = buttonXPosition / dayWidth;
const startDate = addDays(chartStartDate, columnNumber);
setButtonStartDate(startDate);
};
container.addEventListener("mousemove", handleMouseMove);
return () => {
container?.removeEventListener("mousemove", handleMouseMove);
};
}, [buttonXPosition, currentViewData]);
return (
<div
className="relative h-full w-full"
onMouseEnter={() => setIsButtonVisible(true)}
onMouseLeave={() => setIsButtonVisible(false)}
>
<div ref={containerRef} className="h-full w-full" />
{isButtonVisible && (
<Tooltip label={renderFormattedDate(buttonStartDate) ?? ""} disabled={isMobile}>
<button
type="button"
className="absolute top-1/2 grid h-8 w-8 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-sm border border-strong bg-layer-1 p-1.5 text-secondary hover:text-primary"
style={{
marginLeft: `${buttonXPosition}px`,
}}
onClick={handleButtonClick}
>
<AddOutline className="h-3.5 w-3.5" />
</button>
</Tooltip>
)}
</div>
);
});

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { observer } from "mobx-react";
// plane utils
import { cn, renderFormattedDate } from "@plane/utils";
//helpers
//
//hooks
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
type LeftResizableProps = {
enableBlockLeftResize: boolean;
handleBlockDrag: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, dragDirection: "left" | "right" | "move") => void;
isMoving: "left" | "right" | "move" | undefined;
position?: {
marginLeft: number;
width: number;
};
};
export const LeftResizable = observer(function LeftResizable(props: LeftResizableProps) {
const { enableBlockLeftResize, isMoving, handleBlockDrag, position } = props;
const [isHovering, setIsHovering] = useState(false);
const { getDateFromPositionOnGantt } = useTimeLineChartStore();
const date = position ? getDateFromPositionOnGantt(position.marginLeft, 0) : undefined;
const dateString = date ? renderFormattedDate(date) : undefined;
const isLeftResizing = isMoving === "left" || isMoving === "move";
if (!enableBlockLeftResize) return null;
return (
<>
{(isHovering || isLeftResizing) && dateString && (
<div className="absolute -left-36 flex h-full w-32 items-center justify-end text-11 font-regular text-tertiary">
<div className="rounded-sm bg-accent-subtle px-2 py-1">{dateString}</div>
</div>
)}
<div
onMouseDown={(e) => {
handleBlockDrag(e, "left");
}}
onMouseOver={() => {
setIsHovering(true);
}}
onMouseOut={() => {
setIsHovering(false);
}}
className="absolute top-1/2 -left-1.5 z-[6] h-full w-3 -translate-y-1/2 cursor-col-resize rounded-md"
/>
<div
className={cn(
"absolute top-1/2 left-1 z-[5] h-7 w-1 -translate-y-1/2 rounded-xs bg-surface-1 opacity-0 transition-all duration-300 group-hover:opacity-100",
{
"-left-1.5 opacity-100": isLeftResizing,
}
)}
/>
</>
);
});

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { observer } from "mobx-react";
// plane utils
import { cn, renderFormattedDate } from "@plane/utils";
//helpers
//
//hooks
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
type RightResizableProps = {
enableBlockRightResize: boolean;
handleBlockDrag: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, dragDirection: "left" | "right" | "move") => void;
isMoving: "left" | "right" | "move" | undefined;
position?: {
marginLeft: number;
width: number;
};
};
export const RightResizable = observer(function RightResizable(props: RightResizableProps) {
const { enableBlockRightResize, handleBlockDrag, isMoving, position } = props;
const [isHovering, setIsHovering] = useState(false);
const { getDateFromPositionOnGantt } = useTimeLineChartStore();
const date = position ? getDateFromPositionOnGantt(position.marginLeft + position.width, -1) : undefined;
const dateString = date ? renderFormattedDate(date) : undefined;
const isRightResizing = isMoving === "right" || isMoving === "move";
if (!enableBlockRightResize) return null;
return (
<>
{(isHovering || isRightResizing) && dateString && (
<div className="absolute -right-36 z-[10] flex h-full w-32 items-center justify-start text-11 font-regular text-tertiary">
<div className="rounded-sm bg-accent-subtle px-2 py-1">{dateString}</div>
</div>
)}
<div
onMouseDown={(e) => handleBlockDrag(e, "right")}
onMouseOver={() => {
setIsHovering(true);
}}
onMouseOut={() => {
setIsHovering(false);
}}
className="absolute top-1/2 -right-1.5 z-[6] h-full w-3 -translate-y-1/2 cursor-col-resize rounded-md"
/>
<div
className={cn(
"absolute top-1/2 right-1 z-[5] h-7 w-1 -translate-y-1/2 rounded-xs bg-surface-1 opacity-0 transition-all duration-300 group-hover:opacity-100",
{
"-right-1.5 opacity-100": isRightResizing,
}
)}
/>
</>
);
});

View File

@@ -0,0 +1,150 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useRef, useState } from "react";
// Plane
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IBlockUpdateDependencyData, IGanttBlock } from "@plane/types";
// hooks
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
//
import { DEFAULT_BLOCK_WIDTH, SIDEBAR_WIDTH } from "../../constants";
export const useGanttResizable = (
block: IGanttBlock,
resizableRef: React.RefObject<HTMLDivElement | null>,
ganttContainerRef: React.RefObject<HTMLDivElement | null>,
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>
) => {
// refs
const initialPositionRef = useRef<{ marginLeft: number; width: number; offsetX: number }>({
marginLeft: 0,
width: 0,
offsetX: 0,
});
const ganttContainerDimensions = useRef<DOMRect | undefined>(undefined);
const currMouseEvent = useRef<MouseEvent | undefined>(undefined);
// states
const { currentViewData, updateBlockPosition, setIsDragging, getUpdatedPositionAfterDrag } = useTimeLineChartStore();
const [isMoving, setIsMoving] = useState<"left" | "right" | "move" | undefined>();
// handle block resize from the left end
const handleBlockDrag = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
dragDirection: "left" | "right" | "move"
) => {
const ganttContainerElement = ganttContainerRef.current;
if (!currentViewData || !resizableRef.current || !block.position || !ganttContainerElement) return;
if (e.button !== 0) return;
const resizableDiv = resizableRef.current;
ganttContainerDimensions.current = ganttContainerElement.getBoundingClientRect();
const dayWidth = currentViewData.data.dayWidth;
const mouseX = e.clientX - ganttContainerDimensions.current.left - SIDEBAR_WIDTH + ganttContainerElement.scrollLeft;
// record position on drag start
initialPositionRef.current = {
width: block.position.width ?? 0,
marginLeft: block.position.marginLeft ?? 0,
offsetX: mouseX - block.position.marginLeft,
};
const handleOnScroll = () => {
if (currMouseEvent.current) handleMouseMove(currMouseEvent.current);
};
const handleMouseMove = (e: MouseEvent) => {
currMouseEvent.current = e;
setIsMoving(dragDirection);
setIsDragging(true);
if (!ganttContainerDimensions.current) return;
const { left: containerLeft } = ganttContainerDimensions.current;
const mouseX = e.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;
// get Dimensions from dom's style
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;
// If target date does not exist while dragging with left handle the revert to default width
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;
// 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;
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;
}
// block needs to be at least 1 dayWidth Wide
if (width < dayWidth) return;
resizableDiv.style.width = `${width}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;
// call update blockPosition
if (deltaWidth || deltaLeft) updateBlockPosition(block.id, deltaLeft, deltaWidth);
};
// remove event listeners and call updateBlockDates
const handleMouseUp = () => {
setIsMoving(undefined);
document.removeEventListener("mousemove", handleMouseMove);
ganttContainerElement.removeEventListener("scroll", handleOnScroll);
document.removeEventListener("mouseup", handleMouseUp);
// update half blocks only when the missing side of the block is directly dragged
const shouldUpdateHalfBlock =
(dragDirection === "left" && !block.start_date) || (dragDirection === "right" && !block.target_date);
try {
const blockUpdates = getUpdatedPositionAfterDrag(block.id, shouldUpdateHalfBlock);
if (updateBlockDates) updateBlockDates(blockUpdates);
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error",
message: "Something went wrong while updating block dates",
});
}
setIsDragging(false);
};
document.addEventListener("mousemove", handleMouseMove);
ganttContainerElement.addEventListener("scroll", handleOnScroll);
document.addEventListener("mouseup", handleMouseUp);
};
return {
isMoving,
handleBlockDrag,
};
};

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { RefObject } from "react";
import React from "react";
import { observer } from "mobx-react";
// hooks
import type { IGanttBlock } from "@plane/types";
// helpers
import { cn } from "@plane/utils";
// components
import { LeftResizable } from "./blockResizables/left-resizable";
import { RightResizable } from "./blockResizables/right-resizable";
type Props = {
block: IGanttBlock;
blockToRender: (data: any) => React.ReactNode;
handleBlockDrag: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, dragDirection: "left" | "right" | "move") => void;
isMoving: "left" | "right" | "move" | undefined;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableDependency: boolean | ((blockId: string) => boolean);
ganttContainerRef: RefObject<HTMLDivElement | null>;
};
export const ChartDraggable = observer(function ChartDraggable(props: Props) {
const {
block,
blockToRender,
handleBlockDrag,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
isMoving,
} = props;
return (
<div className="group relative z-[5] inline-flex h-full w-full cursor-pointer items-center font-medium transition-all">
<LeftResizable
enableBlockLeftResize={enableBlockLeftResize}
handleBlockDrag={handleBlockDrag}
isMoving={isMoving}
position={block.position}
/>
{/* oxlint-disable-next-line jsx_a11y/no-static-element-interactions */}
<div
className={cn("relative z-[6] flex h-8 w-full items-center rounded-sm", {
"pointer-events-none": isMoving,
})}
onMouseDown={(e) => enableBlockMove && handleBlockDrag(e, "move")}
>
{blockToRender({ ...block.data, meta: block.meta })}
</div>
{/* right resize drag handle */}
<RightResizable
enableBlockRightResize={enableBlockRightResize}
handleBlockDrag={handleBlockDrag}
isMoving={isMoving}
position={block.position}
/>
</div>
);
});

View File

@@ -0,0 +1,8 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export * from "./add-block";
export * from "./draggable";