Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-1140] chore: Gantt pragmatic dnd #4390

Merged
merged 3 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/types/src/module/module_filters.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export type TModuleOrderByOptions =
| "target_date"
| "-target_date"
| "created_at"
| "-created_at";
| "-created_at"
| "sort_order";

export type TModuleLayoutOptions = "list" | "board" | "gantt";

Expand Down
2 changes: 1 addition & 1 deletion web/components/cycles/cycles-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
const { getFilteredCycleIds, getFilteredCompletedCycleIds, loader } = useCycle();
const { searchQuery } = useCycleFilter();
// derived values
const filteredCycleIds = getFilteredCycleIds(projectId);
const filteredCycleIds = getFilteredCycleIds(projectId, layout === "gantt");
const filteredCompletedCycleIds = getFilteredCompletedCycleIds(projectId);

if (loader || !filteredCycleIds)
Expand Down
2 changes: 1 addition & 1 deletion web/components/cycles/gantt-chart/cycles-list-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
enableBlockLeftResize={false}
enableBlockRightResize={false}
enableBlockMove={false}
enableReorder={false}
enableReorder
/>
</div>
);
Expand Down
18 changes: 17 additions & 1 deletion web/components/gantt-chart/chart/main-content.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useRef } from "react";
import { useEffect, useRef } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react";
// hooks
// components
Expand Down Expand Up @@ -62,6 +64,20 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
const ganttContainerRef = useRef<HTMLDivElement>(null);
// chart hook
const { currentView, currentViewData } = useGanttChart();

// Enable Auto Scroll for Ganttlist
useEffect(() => {
const element = ganttContainerRef.current;

if (!element) return;

return combine(
autoScrollForElements({
element,
getAllowedAxis: () => "vertical",
})
);
}, [ganttContainerRef?.current]);
// handling scroll functionality
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
Expand Down
14 changes: 6 additions & 8 deletions web/components/gantt-chart/sidebar/cycles/block.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { MutableRefObject } from "react";
import { observer } from "mobx-react";
import { MoreVertical } from "lucide-react";
// hooks
Expand All @@ -16,12 +16,12 @@ import { findTotalDaysInRange } from "@/helpers/date-time.helper";
type Props = {
block: IGanttBlock;
enableReorder: boolean;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
isDragging: boolean;
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
};

export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
const { block, enableReorder, provided, snapshot } = props;
const { block, enableReorder, isDragging, dragHandleRef } = props;
// store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart();

Expand All @@ -30,12 +30,10 @@ export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
return (
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
"rounded bg-custom-background-80": isDragging,
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
id={`sidebar-block-${block.id}`}
Expand All @@ -50,7 +48,7 @@ export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
ref={dragHandleRef}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
Expand Down
116 changes: 38 additions & 78 deletions web/components/gantt-chart/sidebar/cycles/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
import { MutableRefObject } from "react";
// ui
import { Loader } from "@plane/ui";
// components
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart/types";
import { GanttDnDHOC } from "../gantt-dnd-HOC";
import { handleOrderChange } from "../utils";
import { CyclesSidebarBlock } from "./block";
// types

Expand All @@ -16,85 +18,43 @@ type Props = {
export const CycleGanttSidebar: React.FC<Props> = (props) => {
const { blockUpdateHandler, blocks, enableReorder } = props;

const handleOrderChange = (result: DropResult) => {
if (!blocks) return;

const { source, destination } = result;

// return if dropped outside the list
if (!destination) return;

// return if dropped on the same index
if (source.index === destination.index) return;

let updatedSortOrder = blocks[source.index].sort_order;

// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;

updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}

// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);

// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
const handleOnDrop = (
draggingBlockId: string | undefined,
droppedBlockId: string | undefined,
dropAtEndOfList: boolean
) => {
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blocks, blockUpdateHandler);
};

return (
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<CyclesSidebarBlock
block={block}
enableReorder={enableReorder}
provided={provided}
snapshot={snapshot}
/>
)}
</Draggable>
))
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
</div>
)}
</Droppable>
</DragDropContext>
<div className="h-full">
{blocks ? (
blocks.map((block, index) => (
<GanttDnDHOC
key={block.id}
id={block.id}
isLastChild={index === blocks.length - 1}
isDragEnabled={enableReorder}
onDrop={handleOnDrop}
>
{(isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => (
<CyclesSidebarBlock
block={block}
enableReorder={enableReorder}
isDragging={isDragging}
dragHandleRef={dragHandleRef}
/>
)}
</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>
);
};
104 changes: 104 additions & 0 deletions web/components/gantt-chart/sidebar/gantt-dnd-HOC.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { MutableRefObject, 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 { DropIndicator } from "@plane/ui";
import { HIGHLIGHT_WITH_LINE, highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";

type Props = {
id: string;
isLastChild: boolean;
isDragEnabled: boolean;
children: (isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => JSX.Element;
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);
const dragHandleRef = useRef<HTMLButtonElement | null>(null);

useEffect(() => {
const element = blockRef.current;
const dragHandleElement = dragHandleRef.current;

if (!element) return;

return combine(
draggable({
element,
canDrag: () => isDragEnabled,
dragHandle: dragHandleElement ?? undefined,
getInitialData: () => ({ id }),
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
},
}),
dropTargetForElements({
element,
canDrop: ({ source }) => source?.data?.id !== id,
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, dragHandleRef?.current, isLastChild, onDrop]);

useOutsideClickDetector(blockRef, () => blockRef?.current?.classList?.remove(HIGHLIGHT_WITH_LINE));

return (
<div id={`issue-draggable-${id}`} className={"relative"} ref={blockRef}>
<DropIndicator classNames="absolute top-0" isVisible={instruction === "DRAG_OVER"} />
{children(isDragging, dragHandleRef)}
{isLastChild && <DropIndicator isVisible={instruction === "DRAG_BELOW"} />}
</div>
);
});
1 change: 0 additions & 1 deletion web/components/gantt-chart/sidebar/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from "./cycles";
export * from "./issues";
export * from "./modules";
export * from "./project-views";
export * from "./root";
16 changes: 7 additions & 9 deletions web/components/gantt-chart/sidebar/issues/block.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import React, { MutableRefObject } from "react";
import { observer } from "mobx-react";
import { MoreVertical } from "lucide-react";
// hooks
Expand All @@ -17,12 +17,12 @@ import { IGanttBlock } from "../../types";
type Props = {
block: IGanttBlock;
enableReorder: boolean;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
isDragging: boolean;
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
};

export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
const { block, enableReorder, provided, snapshot } = props;
export const IssuesSidebarBlock = observer((props: Props) => {
const { block, enableReorder, isDragging, dragHandleRef } = props;
// store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart();
const { getIsIssuePeeked } = useIssueDetail();
Expand All @@ -32,15 +32,13 @@ export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
return (
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
"rounded bg-custom-background-80": isDragging,
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(
block.data.id
),
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
Expand All @@ -54,7 +52,7 @@ export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
ref={dragHandleRef}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
Expand Down
Loading
Loading