feat: init
This commit is contained in:
117
apps/web/core/components/gantt-chart/sidebar/gantt-dnd-HOC.tsx
Normal file
117
apps/web/core/components/gantt-chart/sidebar/gantt-dnd-HOC.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
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((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>
|
||||
);
|
||||
});
|
||||
3
apps/web/core/components/gantt-chart/sidebar/index.ts
Normal file
3
apps/web/core/components/gantt-chart/sidebar/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./issues";
|
||||
export * from "./modules";
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,89 @@
|
||||
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((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 bg-custom-background-80": isDragging,
|
||||
"rounded-l border border-r-0 border-custom-primary-70": getIsIssuePeeked(block.data.id),
|
||||
"border border-r-0 border-custom-border-400": isIssueFocused,
|
||||
})}
|
||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
>
|
||||
<Row
|
||||
className={cn("group w-full flex items-center gap-2 pr-4", {
|
||||
"bg-custom-background-90": isBlockHoveredOn,
|
||||
"bg-custom-primary-100/5 hover:bg-custom-primary-100/10": isIssueSelected,
|
||||
"bg-custom-primary-100/10": isIssueSelected && isBlockHoveredOn,
|
||||
})}
|
||||
style={{
|
||||
height: `${BLOCK_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
{enableSelection && selectionHelpers && (
|
||||
<div className="flex items-center gap-2 absolute left-1">
|
||||
<MultipleSelectEntityAction
|
||||
className={cn(
|
||||
"opacity-0 pointer-events-none group-hover/list-block:opacity-100 group-hover/list-block:pointer-events-auto transition-opacity",
|
||||
{
|
||||
"opacity-100 pointer-events-auto": 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-sm text-custom-text-200">
|
||||
<span>
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./sidebar";
|
||||
130
apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx
Normal file
130
apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import type { RefObject } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
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 { ETimeLineTypeType } from "../../contexts";
|
||||
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>;
|
||||
blockIds: string[];
|
||||
enableReorder: boolean;
|
||||
enableSelection: boolean;
|
||||
showAllBlocks?: boolean;
|
||||
selectionHelpers?: TSelectionHelper;
|
||||
isEpic?: boolean;
|
||||
};
|
||||
|
||||
export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
blockUpdateHandler,
|
||||
blockIds,
|
||||
enableReorder,
|
||||
enableSelection,
|
||||
loadMoreBlocks,
|
||||
canLoadMoreBlocks,
|
||||
ganttContainerRef,
|
||||
showAllBlocks = false,
|
||||
selectionHelpers,
|
||||
isEpic = false,
|
||||
} = props;
|
||||
|
||||
const { getBlockById } = useTimeLineChart(ETimeLineTypeType.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 md:h-8 w-full items-center justify-between gap-1.5 rounded md:px-1 px-4 py-1.5 bg-custom-background-80 animate-pulse" />
|
||||
</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,58 @@
|
||||
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: React.FC<Props> = observer((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 bg-custom-background-80": isDragging,
|
||||
})}
|
||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
>
|
||||
<Row
|
||||
id={`sidebar-block-${block.id}`}
|
||||
className={cn("group w-full flex items-center gap-2 pr-4", {
|
||||
"bg-custom-background-90": 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-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./sidebar";
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import type { IBlockUpdateData } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
// hooks
|
||||
import { useTimeLineChart } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { ETimeLineTypeType } from "../../contexts";
|
||||
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: React.FC<Props> = observer((props) => {
|
||||
const { blockUpdateHandler, blockIds, enableReorder } = props;
|
||||
|
||||
const { getBlockById } = useTimeLineChart(ETimeLineTypeType.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>
|
||||
);
|
||||
});
|
||||
103
apps/web/core/components/gantt-chart/sidebar/root.tsx
Normal file
103
apps/web/core/components/gantt-chart/sidebar/root.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
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>;
|
||||
enableReorder: boolean | ((blockId: string) => boolean);
|
||||
enableSelection: boolean | ((blockId: string) => boolean);
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
title: string;
|
||||
quickAdd?: React.ReactNode | undefined;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
isEpic?: boolean;
|
||||
};
|
||||
|
||||
export const GanttChartSidebar: React.FC<Props> = observer((props) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
blockIds,
|
||||
blockUpdateHandler,
|
||||
enableReorder,
|
||||
enableSelection,
|
||||
sidebarToRender,
|
||||
loadMoreBlocks,
|
||||
canLoadMoreBlocks,
|
||||
ganttContainerRef,
|
||||
title,
|
||||
quickAdd,
|
||||
selectionHelpers,
|
||||
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 min-h-full h-max flex-shrink-0 border-r-[0.5px] border-custom-border-200 bg-custom-background-100"
|
||||
style={{
|
||||
width: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
variant={ERowVariant.HUGGING}
|
||||
>
|
||||
<Row
|
||||
className="group/list-header box-border flex-shrink-0 flex items-end justify-between gap-2 border-b-[0.5px] border-custom-border-200 pb-2 pr-4 text-sm font-medium text-custom-text-300 sticky top-0 z-10 bg-custom-background-100"
|
||||
style={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
<div className={cn("flex items-center gap-2")}>
|
||||
{enableSelection && (
|
||||
<div className="flex-shrink-0 flex items-center w-3.5 absolute left-1">
|
||||
<MultipleSelectGroupAction
|
||||
className={cn(
|
||||
"size-3.5 opacity-0 pointer-events-none group-hover/list-header:opacity-100 group-hover/list-header:pointer-events-auto !outline-none",
|
||||
{
|
||||
"opacity-100 pointer-events-auto": !isGroupSelectionEmpty,
|
||||
}
|
||||
)}
|
||||
groupID={GANTT_SELECT_GROUP}
|
||||
selectionHelpers={selectionHelpers}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<h6>{title}</h6>
|
||||
</div>
|
||||
<h6>{t("common.duration")}</h6>
|
||||
</Row>
|
||||
|
||||
<Row variant={ERowVariant.HUGGING} className="min-h-full h-max bg-custom-background-100">
|
||||
{sidebarToRender &&
|
||||
sidebarToRender({
|
||||
title,
|
||||
blockUpdateHandler,
|
||||
blockIds,
|
||||
enableReorder,
|
||||
enableSelection,
|
||||
canLoadMoreBlocks,
|
||||
ganttContainerRef,
|
||||
loadMoreBlocks,
|
||||
selectionHelpers,
|
||||
isEpic,
|
||||
})}
|
||||
</Row>
|
||||
{quickAdd ? quickAdd : null}
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
42
apps/web/core/components/gantt-chart/sidebar/utils.ts
Normal file
42
apps/web/core/components/gantt-chart/sidebar/utils.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
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 | undefined) => 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