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
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:
121
apps/web/core/components/gantt-chart/sidebar/gantt-dnd-HOC.tsx
Normal file
121
apps/web/core/components/gantt-chart/sidebar/gantt-dnd-HOC.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
9
apps/web/core/components/gantt-chart/sidebar/index.ts
Normal file
9
apps/web/core/components/gantt-chart/sidebar/index.ts
Normal 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";
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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";
|
||||
134
apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx
Normal file
134
apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
109
apps/web/core/components/gantt-chart/sidebar/root.tsx
Normal file
109
apps/web/core/components/gantt-chart/sidebar/root.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
48
apps/web/core/components/gantt-chart/sidebar/utils.ts
Normal file
48
apps/web/core/components/gantt-chart/sidebar/utils.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user