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,121 @@
/**
* 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 { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
import { observer } from "mobx-react";
import { useOutsideClickDetector } from "@plane/hooks";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { DropIndicator } from "@plane/ui";
import { HIGHLIGHT_WITH_LINE, highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
type Props = {
id: string;
isLastChild: boolean;
isDragEnabled: boolean;
children: (isDragging: boolean) => React.ReactNode;
onDrop: (draggingBlockId: string | undefined, droppedBlockId: string | undefined, dropAtEndOfList: boolean) => void;
};
export const GanttDnDHOC = observer(function GanttDnDHOC(props: Props) {
const { id, isLastChild, children, onDrop, isDragEnabled } = props;
// states
const [isDragging, setIsDragging] = useState(false);
const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined);
// refs
const blockRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const element = blockRef.current;
if (!element) return;
return combine(
draggable({
element,
canDrag: () => isDragEnabled,
getInitialData: () => ({ id, dragInstanceId: "GANTT_REORDER" }),
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
},
}),
dropTargetForElements({
element,
canDrop: ({ source }) => source?.data?.id !== id && source?.data?.dragInstanceId === "GANTT_REORDER",
getData: ({ input, element }) => {
const data = { id };
// attach instruction for last in list
return attachInstruction(data, {
input,
element,
currentLevel: 0,
indentPerLevel: 0,
mode: isLastChild ? "last-in-group" : "standard",
});
},
onDrag: ({ self }) => {
const extractedInstruction = extractInstruction(self?.data)?.type;
// check if the highlight is to be shown above or below
setInstruction(
extractedInstruction
? extractedInstruction === "reorder-below" && isLastChild
? "DRAG_BELOW"
: "DRAG_OVER"
: undefined
);
},
onDragLeave: () => {
setInstruction(undefined);
},
onDrop: ({ self, source }) => {
setInstruction(undefined);
const extractedInstruction = extractInstruction(self?.data)?.type;
const currentInstruction = extractedInstruction
? extractedInstruction === "reorder-below" && isLastChild
? "DRAG_BELOW"
: "DRAG_OVER"
: undefined;
if (!currentInstruction) return;
const sourceId = source?.data?.id as string | undefined;
const destinationId = self?.data?.id as string | undefined;
onDrop(sourceId, destinationId, currentInstruction === "DRAG_BELOW");
highlightIssueOnDrop(source?.element?.id, false, true);
},
})
);
}, [blockRef?.current, isLastChild, onDrop]);
useOutsideClickDetector(blockRef, () => blockRef?.current?.classList?.remove(HIGHLIGHT_WITH_LINE));
return (
<div
id={`draggable-${id}`}
className={"relative"}
ref={blockRef}
onDragStart={() => {
if (!isDragEnabled) {
setToast({
title: "Warning!",
type: TOAST_TYPE.WARNING,
message: "Drag and drop is only enabled when sorted by manual",
});
}
}}
>
<DropIndicator classNames="absolute top-0" isVisible={instruction === "DRAG_OVER"} />
{children(isDragging)}
{isLastChild && <DropIndicator isVisible={instruction === "DRAG_BELOW"} />}
</div>
);
});

View File

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

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import type { IGanttBlock } from "@plane/types";
import { Row } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { MultipleSelectEntityAction } from "@/components/core/multiple-select";
import { IssueGanttSidebarBlock } from "@/components/issues/issue-layouts/gantt/blocks";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import type { TSelectionHelper } from "@/hooks/use-multiple-select";
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
// local imports
import { BLOCK_HEIGHT, GANTT_SELECT_GROUP } from "../../constants";
type Props = {
block: IGanttBlock;
enableSelection: boolean;
isDragging: boolean;
selectionHelpers?: TSelectionHelper;
isEpic?: boolean;
};
export const IssuesSidebarBlock = observer(function IssuesSidebarBlock(props: Props) {
const { block, enableSelection, isDragging, selectionHelpers, isEpic = false } = props;
// store hooks
const { updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore();
const { getIsIssuePeeked } = useIssueDetail();
const isBlockComplete = !!block?.start_date && !!block?.target_date;
const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
if (!block?.data) return null;
const isIssueSelected = selectionHelpers?.getIsEntitySelected(block.id);
const isIssueFocused = selectionHelpers?.getIsEntityActive(block.id);
const isBlockHoveredOn = isBlockActive(block.id);
return (
<div
className={cn("group/list-block", {
"rounded-sm bg-layer-1": isDragging,
"rounded-l-sm border border-r-0 border-accent-strong": getIsIssuePeeked(block.data.id),
"border border-r-0 border-strong-1": isIssueFocused,
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
>
<Row
className={cn(
"group flex w-full items-center gap-2 bg-layer-transparent pr-4 hover:bg-layer-transparent-hover",
{
"bg-layer-transparent-hover": isBlockHoveredOn,
"bg-accent-primary/5 hover:bg-accent-primary/10": isIssueSelected,
"bg-accent-primary/10": isIssueSelected && isBlockHoveredOn,
}
)}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableSelection && selectionHelpers && (
<div className="absolute left-1 flex items-center gap-2">
<MultipleSelectEntityAction
className={cn(
"pointer-events-none opacity-0 transition-opacity group-hover/list-block:pointer-events-auto group-hover/list-block:opacity-100",
{
"pointer-events-auto opacity-100": isIssueSelected,
}
)}
groupId={GANTT_SELECT_GROUP}
id={block.id}
selectionHelpers={selectionHelpers}
/>
</div>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<IssueGanttSidebarBlock issueId={block.data.id} isEpic={isEpic} />
</div>
{duration && (
<div className="flex-shrink-0 text-13 text-secondary">
<span>
{duration} day{duration > 1 ? "s" : ""}
</span>
</div>
)}
</div>
</Row>
</div>
);
});

View File

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

View File

@@ -0,0 +1,134 @@
/**
* 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 { useState } from "react";
import { observer } from "mobx-react";
// ui
import { GANTT_TIMELINE_TYPE } from "@plane/types";
import type { IBlockUpdateData } from "@plane/types";
import { Loader } from "@plane/ui";
// components
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
import { GanttLayoutListItemLoader } from "@/components/ui/loader/layouts/gantt-layout-loader";
//hooks
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
import type { TSelectionHelper } from "@/hooks/use-multiple-select";
// local imports
import { useTimeLineChart } from "../../../../hooks/use-timeline-chart";
import { GanttDnDHOC } from "../gantt-dnd-HOC";
import { handleOrderChange } from "../utils";
import { IssuesSidebarBlock } from "./block";
type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
canLoadMoreBlocks?: boolean;
loadMoreBlocks?: () => void;
ganttContainerRef: RefObject<HTMLDivElement | null>;
blockIds: string[];
enableReorder: boolean;
enableSelection: boolean;
showAllBlocks?: boolean;
selectionHelpers?: TSelectionHelper;
isEpic?: boolean;
};
export const IssueGanttSidebar = observer(function IssueGanttSidebar(props: Props) {
const {
blockUpdateHandler,
blockIds,
enableReorder,
enableSelection,
loadMoreBlocks,
canLoadMoreBlocks,
ganttContainerRef,
showAllBlocks = false,
selectionHelpers,
isEpic = false,
} = props;
const { getBlockById } = useTimeLineChart(GANTT_TIMELINE_TYPE.ISSUE);
const {
issues: { getIssueLoader },
} = useIssuesStore();
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
const isPaginating = !!getIssueLoader();
useIntersectionObserver(
ganttContainerRef,
isPaginating ? null : intersectionElement,
loadMoreBlocks,
"100% 0% 100% 0%"
);
const handleOnDrop = (
draggingBlockId: string | undefined,
droppedBlockId: string | undefined,
dropAtEndOfList: boolean
) => {
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blockIds, getBlockById, blockUpdateHandler);
};
return (
<div>
{blockIds ? (
<>
{blockIds.map((blockId, index) => {
const block = getBlockById(blockId);
const isBlockVisibleOnSidebar = 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 && !isBlockVisibleOnSidebar)) return;
return (
<RenderIfVisible
key={block.id}
root={ganttContainerRef}
horizontalOffset={100}
verticalOffset={200}
shouldRecordHeights={false}
placeholderChildren={<GanttLayoutListItemLoader />}
>
<GanttDnDHOC
id={block.id}
isLastChild={index === blockIds.length - 1}
isDragEnabled={enableReorder}
onDrop={handleOnDrop}
>
{(isDragging: boolean) => (
<IssuesSidebarBlock
block={block}
enableSelection={enableSelection}
isDragging={isDragging}
selectionHelpers={selectionHelpers}
isEpic={isEpic}
/>
)}
</GanttDnDHOC>
</RenderIfVisible>
);
})}
{canLoadMoreBlocks && (
<div ref={setIntersectionElement} className="p-2">
<div className="flex h-10 w-full animate-pulse items-center justify-between gap-1.5 rounded-sm bg-layer-1 px-4 py-1.5 md:h-8 md:px-1" />
</div>
)}
</>
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
</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 { observer } from "mobx-react";
// Plane
import { Row } from "@plane/ui";
// components
import { cn } from "@plane/utils";
import { BLOCK_HEIGHT } from "@/components/gantt-chart/constants";
import { ModuleGanttSidebarBlock } from "@/components/modules";
// helpers
// hooks
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
type Props = {
blockId: string;
isDragging: boolean;
};
export const ModulesSidebarBlock = observer(function ModulesSidebarBlock(props: Props) {
const { blockId, isDragging } = props;
// store hooks
const { getBlockById, updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore();
const block = getBlockById(blockId);
if (!block) return <></>;
const isBlockComplete = !!block.start_date && !!block.target_date;
const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
return (
<div
className={cn({
"rounded-sm bg-layer-1": isDragging,
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
>
<Row
id={`sidebar-block-${block.id}`}
className={cn(
"group flex w-full items-center gap-2 bg-layer-transparent pr-4 hover:bg-layer-transparent-hover",
{
"bg-transparent-hover": isBlockActive(block.id),
}
)}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<ModuleGanttSidebarBlock moduleId={block.data.id} />
</div>
{duration !== undefined && (
<div className="flex-shrink-0 text-13 text-secondary">
{duration} day{duration > 1 ? "s" : ""}
</div>
)}
</div>
</Row>
</div>
);
});

View File

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

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// ui
import { GANTT_TIMELINE_TYPE } from "@plane/types";
import type { IBlockUpdateData } from "@plane/types";
import { Loader } from "@plane/ui";
// components
// hooks
import { useTimeLineChart } from "@/hooks/use-timeline-chart";
import { GanttDnDHOC } from "../gantt-dnd-HOC";
import { handleOrderChange } from "../utils";
import { ModulesSidebarBlock } from "./block";
// types
type Props = {
title: string;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockIds: string[];
enableReorder: boolean;
};
export const ModuleGanttSidebar = observer(function ModuleGanttSidebar(props: Props) {
const { blockUpdateHandler, blockIds, enableReorder } = props;
const { getBlockById } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE);
const handleOnDrop = (
draggingBlockId: string | undefined,
droppedBlockId: string | undefined,
dropAtEndOfList: boolean
) => {
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blockIds, getBlockById, blockUpdateHandler);
};
return (
<div className="h-full">
{blockIds ? (
blockIds.map((blockId, index) => (
<GanttDnDHOC
key={blockId}
id={blockId}
isLastChild={index === blockIds.length - 1}
isDragEnabled={enableReorder}
onDrop={handleOnDrop}
>
{(isDragging: boolean) => <ModulesSidebarBlock blockId={blockId} isDragging={isDragging} />}
</GanttDnDHOC>
))
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
</div>
);
});

View File

@@ -0,0 +1,109 @@
/**
* 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 { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
// components
import type { IBlockUpdateData } from "@plane/types";
import { Row, ERowVariant } from "@plane/ui";
import { cn } from "@plane/utils";
import { MultipleSelectGroupAction } from "@/components/core/multiple-select";
// helpers
// hooks
import type { TSelectionHelper } from "@/hooks/use-multiple-select";
// constants
import { GANTT_SELECT_GROUP, HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
type Props = {
blockIds: string[];
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
canLoadMoreBlocks?: boolean;
loadMoreBlocks?: () => void;
ganttContainerRef: RefObject<HTMLDivElement | null>;
enableReorder: boolean | ((blockId: string) => boolean);
enableSelection: boolean | ((blockId: string) => boolean);
sidebarToRender: (props: any) => React.ReactNode;
title: string;
selectionHelpers: TSelectionHelper;
showAllBlocks?: boolean;
isEpic?: boolean;
};
export const GanttChartSidebar = observer(function GanttChartSidebar(props: Props) {
const { t } = useTranslation();
const {
blockIds,
blockUpdateHandler,
enableReorder,
enableSelection,
sidebarToRender,
loadMoreBlocks,
canLoadMoreBlocks,
ganttContainerRef,
title,
selectionHelpers,
showAllBlocks = false,
isEpic = false,
} = props;
const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(GANTT_SELECT_GROUP) === "empty";
return (
<Row
// DO NOT REMOVE THE ID
id="gantt-sidebar"
className="sticky left-0 z-10 h-max min-h-full flex-shrink-0 border-r-[0.5px] border-subtle-1 bg-surface-1"
style={{
width: `${SIDEBAR_WIDTH}px`,
}}
variant={ERowVariant.HUGGING}
>
<Row
className="group/list-header sticky top-0 z-10 box-border flex flex-shrink-0 items-end justify-between gap-2 border-b-[0.5px] border-subtle-1 bg-surface-1 pr-4 pb-2 text-13 font-medium text-tertiary"
style={{
height: `${HEADER_HEIGHT}px`,
}}
>
<div className={cn("flex items-center gap-2")}>
{enableSelection && (
<div className="absolute left-1 flex w-3.5 flex-shrink-0 items-center">
<MultipleSelectGroupAction
className={cn(
"pointer-events-none size-3.5 opacity-0 !outline-none group-hover/list-header:pointer-events-auto group-hover/list-header:opacity-100",
{
"pointer-events-auto opacity-100": !isGroupSelectionEmpty,
}
)}
groupID={GANTT_SELECT_GROUP}
selectionHelpers={selectionHelpers}
/>
</div>
)}
<h6>{title}</h6>
</div>
<h6>{t("common.duration")}</h6>
</Row>
<Row variant={ERowVariant.HUGGING} className="h-max min-h-full bg-surface-1">
{sidebarToRender &&
sidebarToRender({
title,
blockUpdateHandler,
blockIds,
enableReorder,
enableSelection,
canLoadMoreBlocks,
ganttContainerRef,
loadMoreBlocks,
selectionHelpers,
showAllBlocks,
isEpic,
})}
</Row>
</Row>
);
});

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { ChartDataType, IBlockUpdateData, IGanttBlock } from "@plane/types";
export const handleOrderChange = (
draggingBlockId: string | undefined,
droppedBlockId: string | undefined,
dropAtEndOfList: boolean,
blockIds: string[] | null,
getBlockById: (id: string, currentViewData?: ChartDataType) => IGanttBlock,
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void
) => {
if (!blockIds || !draggingBlockId || !droppedBlockId) return;
const sourceBlockIndex = blockIds.findIndex((id) => id === draggingBlockId);
const destinationBlockIndex = dropAtEndOfList ? blockIds.length : blockIds.findIndex((id) => id === droppedBlockId);
// return if dropped outside the list
if (sourceBlockIndex === -1 || destinationBlockIndex === -1 || sourceBlockIndex === destinationBlockIndex) return;
let updatedSortOrder = getBlockById(blockIds[sourceBlockIndex])?.sort_order ?? 0;
// update the sort order to the lowest if dropped at the top
if (destinationBlockIndex === 0) updatedSortOrder = (getBlockById(blockIds[0])?.sort_order ?? 0) - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destinationBlockIndex === blockIds.length)
updatedSortOrder = (getBlockById(blockIds[blockIds.length - 1])?.sort_order ?? 0) + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = getBlockById(blockIds[destinationBlockIndex])?.sort_order ?? 0;
const relativeDestinationSortingOrder = getBlockById(blockIds[destinationBlockIndex - 1])?.sort_order ?? 0;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(getBlockById(blockIds[sourceBlockIndex])?.data, {
sort_order: {
destinationIndex: destinationBlockIndex,
newSortOrder: updatedSortOrder,
sourceIndex: sourceBlockIndex,
},
});
};