mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-1140] chore: Gantt pragmatic dnd (#4390)
* Gantt Drag and drop migration and enable Dnd in Modules and Cycles Gantt * fix minor UI and code issues
This commit is contained in:
parent
b8f1734738
commit
13e6a67321
@ -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";
|
||||
|
||||
|
@ -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)
|
||||
|
@ -61,7 +61,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
||||
enableBlockLeftResize={false}
|
||||
enableBlockRightResize={false}
|
||||
enableBlockMove={false}
|
||||
enableReorder={false}
|
||||
enableReorder
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -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
|
||||
@ -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;
|
||||
|
@ -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
|
||||
@ -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();
|
||||
|
||||
@ -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}`}
|
||||
@ -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" />
|
||||
|
@ -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
|
||||
|
||||
@ -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
web/components/gantt-chart/sidebar/gantt-dnd-HOC.tsx
Normal file
104
web/components/gantt-chart/sidebar/gantt-dnd-HOC.tsx
Normal file
@ -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,5 +1,4 @@
|
||||
export * from "./cycles";
|
||||
export * from "./issues";
|
||||
export * from "./modules";
|
||||
export * from "./project-views";
|
||||
export * from "./root";
|
||||
|
@ -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
|
||||
@ -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();
|
||||
@ -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", {
|
||||
@ -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" />
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
||||
import { MutableRefObject } from "react";
|
||||
// components
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// types
|
||||
import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types";
|
||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||
import { handleOrderChange } from "../utils";
|
||||
import { IssuesSidebarBlock } from "./block";
|
||||
|
||||
type Props = {
|
||||
@ -16,92 +18,50 @@ type Props = {
|
||||
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = 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 ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
||||
<>
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => {
|
||||
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
|
||||
<div>
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => {
|
||||
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 (!showAllBlocks && !isBlockVisibleOnSidebar) return;
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={`sidebar-block-${block.id}`}
|
||||
draggableId={`sidebar-block-${block.id}`}
|
||||
index={index}
|
||||
isDragDisabled={!enableReorder}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<IssuesSidebarBlock
|
||||
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>
|
||||
return (
|
||||
<GanttDnDHOC
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blocks.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{(isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => (
|
||||
<IssuesSidebarBlock
|
||||
block={block}
|
||||
enableReorder={enableReorder}
|
||||
isDragging={isDragging}
|
||||
dragHandleRef={dragHandleRef}
|
||||
/>
|
||||
)}
|
||||
{droppableProvided.placeholder}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
||||
@ -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 ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
const { block, enableReorder, provided, snapshot } = props;
|
||||
const { block, enableReorder, isDragging, dragHandleRef } = props;
|
||||
// store hooks
|
||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||
|
||||
@ -30,12 +30,10 @@ export const ModulesSidebarBlock: 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}`}
|
||||
@ -50,7 +48,7 @@ export const ModulesSidebarBlock: 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" />
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
||||
import { MutableRefObject } from "react";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||
import { handleOrderChange } from "../utils";
|
||||
import { ModulesSidebarBlock } from "./block";
|
||||
// types
|
||||
|
||||
@ -16,85 +18,43 @@ type Props = {
|
||||
export const ModuleGanttSidebar: 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) => (
|
||||
<ModulesSidebarBlock
|
||||
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>) => (
|
||||
<ModulesSidebarBlock
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -1,105 +0,0 @@
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart/types";
|
||||
import { IssuesSidebarBlock } from "./issues/block";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blocks: IGanttBlock[] | null;
|
||||
enableReorder: boolean;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
};
|
||||
|
||||
export const ProjectViewGanttSidebar: 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,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleOrderChange}>
|
||||
<Droppable droppableId="gantt-sidebar">
|
||||
{(droppableProvided) => (
|
||||
<div
|
||||
className="mt-3 max-h-full overflow-y-auto pl-2.5"
|
||||
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) => (
|
||||
<IssuesSidebarBlock
|
||||
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>
|
||||
);
|
||||
};
|
@ -34,7 +34,7 @@ export const GanttChartSidebar: React.FC<Props> = (props) => {
|
||||
<h6>Duration</h6>
|
||||
</div>
|
||||
|
||||
<div className="min-h-full h-max bg-custom-background-100 overflow-x-hidden overflow-y-auto">
|
||||
<div className="min-h-full h-max bg-custom-background-100 overflow-hidden">
|
||||
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
|
||||
</div>
|
||||
{quickAdd ? quickAdd : null}
|
||||
|
42
web/components/gantt-chart/sidebar/utils.ts
Normal file
42
web/components/gantt-chart/sidebar/utils.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||
|
||||
export const handleOrderChange = (
|
||||
draggingBlockId: string | undefined,
|
||||
droppedBlockId: string | undefined,
|
||||
dropAtEndOfList: boolean,
|
||||
blocks: IGanttBlock[] | null,
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void
|
||||
) => {
|
||||
if (!blocks || !draggingBlockId || !droppedBlockId) return;
|
||||
|
||||
const sourceBlockIndex = blocks.findIndex((block) => block.id === draggingBlockId);
|
||||
const destinationBlockIndex = dropAtEndOfList
|
||||
? blocks.length
|
||||
: blocks.findIndex((block) => block.id === droppedBlockId);
|
||||
|
||||
// return if dropped outside the list
|
||||
if (sourceBlockIndex === -1 || destinationBlockIndex === -1 || sourceBlockIndex === destinationBlockIndex) return;
|
||||
|
||||
let updatedSortOrder = blocks[sourceBlockIndex].sort_order;
|
||||
|
||||
// update the sort order to the lowest if dropped at the top
|
||||
if (destinationBlockIndex === 0) updatedSortOrder = blocks[0].sort_order - 1000;
|
||||
// update the sort order to the highest if dropped at the bottom
|
||||
else if (destinationBlockIndex === blocks.length) 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[destinationBlockIndex].sort_order;
|
||||
const relativeDestinationSortingOrder = blocks[destinationBlockIndex - 1].sort_order;
|
||||
|
||||
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
||||
}
|
||||
|
||||
// call the block update handler with the updated sort order, new and old index
|
||||
blockUpdateHandler(blocks[sourceBlockIndex].data, {
|
||||
sort_order: {
|
||||
destinationIndex: destinationBlockIndex,
|
||||
newSortOrder: updatedSortOrder,
|
||||
sourceIndex: sourceBlockIndex,
|
||||
},
|
||||
});
|
||||
};
|
@ -6,6 +6,7 @@ import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
||||
// hooks
|
||||
import { ControlLink, DropIndicator, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
import { HIGHLIGHT_CLASS } from "@/components/issues/issue-layouts/utils";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { useApplication, useIssueDetail, useKanbanView, useProject } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
@ -133,7 +134,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
||||
const isDragAllowed = !isDragDisabled && !issue?.tempId && canEditIssueProperties;
|
||||
|
||||
useOutsideClickDetector(cardRef, () => {
|
||||
cardRef?.current?.classList?.remove("highlight");
|
||||
cardRef?.current?.classList?.remove(HIGHLIGHT_CLASS);
|
||||
});
|
||||
|
||||
// Make Issue block both as as Draggable and,
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
} from "@plane/types";
|
||||
import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
|
||||
import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
@ -20,12 +21,7 @@ import { cn } from "@/helpers/common.helper";
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
//components
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import {
|
||||
KanbanDropLocation,
|
||||
getSourceFromDropPayload,
|
||||
getDestinationFromDropPayload,
|
||||
highlightIssueOnDrop,
|
||||
} from "./utils";
|
||||
import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils";
|
||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||
|
||||
interface IKanbanGroup {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import pull from "lodash/pull";
|
||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||
import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
|
||||
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
|
||||
|
||||
@ -212,18 +211,3 @@ export const handleDragDrop = async (
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method finds the DOM element with elementId, scrolls to it and highlights the issue block
|
||||
* @param elementId
|
||||
* @param shouldScrollIntoView
|
||||
*/
|
||||
export const highlightIssueOnDrop = (elementId: string | undefined, shouldScrollIntoView = true) => {
|
||||
setTimeout(async () => {
|
||||
const sourceElementId = elementId ?? "";
|
||||
const sourceElement = document.getElementById(sourceElementId);
|
||||
sourceElement?.classList?.add("highlight");
|
||||
if (shouldScrollIntoView && sourceElement)
|
||||
await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 });
|
||||
}, 200);
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||
import { ContrastIcon } from "lucide-react";
|
||||
import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types";
|
||||
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||
@ -16,6 +17,9 @@ import { IStateStore } from "@/store/state.store";
|
||||
// constants
|
||||
// types
|
||||
|
||||
export const HIGHLIGHT_CLASS = "highlight";
|
||||
export const HIGHLIGHT_WITH_LINE = "highlight-with-line";
|
||||
|
||||
export const isWorkspaceLevel = (type: EIssuesStoreType) =>
|
||||
[EIssuesStoreType.PROFILE, EIssuesStoreType.GLOBAL].includes(type) ? true : false;
|
||||
|
||||
@ -240,3 +244,22 @@ const getCreatedByColumns = (member: IMemberRootStore) => {
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method finds the DOM element with elementId, scrolls to it and highlights the issue block
|
||||
* @param elementId
|
||||
* @param shouldScrollIntoView
|
||||
*/
|
||||
export const highlightIssueOnDrop = (
|
||||
elementId: string | undefined,
|
||||
shouldScrollIntoView = true,
|
||||
shouldHighLightWithLine = false
|
||||
) => {
|
||||
setTimeout(async () => {
|
||||
const sourceElementId = elementId ?? "";
|
||||
const sourceElement = document.getElementById(sourceElementId);
|
||||
sourceElement?.classList?.add(shouldHighLightWithLine ? HIGHLIGHT_WITH_LINE : HIGHLIGHT_CLASS);
|
||||
if (shouldScrollIntoView && sourceElement)
|
||||
await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 });
|
||||
}, 200);
|
||||
};
|
||||
|
@ -19,6 +19,7 @@ export const ModuleOrderByDropdown: React.FC<Props> = (props) => {
|
||||
const orderByDetails = MODULE_ORDER_BY_OPTIONS.find((option) => value?.includes(option.key));
|
||||
|
||||
const isDescending = value?.[0] === "-";
|
||||
const isManual = value?.includes("sort_order");
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
@ -38,7 +39,7 @@ export const ModuleOrderByDropdown: React.FC<Props> = (props) => {
|
||||
key={option.key}
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (isDescending) onChange(`-${option.key}` as TModuleOrderByOptions);
|
||||
if (isDescending && !isManual) onChange(`-${option.key}` as TModuleOrderByOptions);
|
||||
else onChange(option.key);
|
||||
}}
|
||||
>
|
||||
@ -46,25 +47,29 @@ export const ModuleOrderByDropdown: React.FC<Props> = (props) => {
|
||||
{value?.includes(option.key) && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
<hr className="my-2 border-custom-border-200" />
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions);
|
||||
}}
|
||||
>
|
||||
Ascending
|
||||
{!isDescending && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions);
|
||||
}}
|
||||
>
|
||||
Descending
|
||||
{isDescending && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
{!isManual && (
|
||||
<>
|
||||
<hr className="my-2 border-custom-border-200" />
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions);
|
||||
}}
|
||||
>
|
||||
Ascending
|
||||
{!isDescending && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions);
|
||||
}}
|
||||
>
|
||||
Descending
|
||||
{isDescending && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
)}
|
||||
</CustomMenu>
|
||||
);
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import { IModule } from "@plane/types";
|
||||
import { GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "@/components/gantt-chart";
|
||||
import { ModuleGanttBlock } from "@/components/modules";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { useModule, useProject } from "@/hooks/store";
|
||||
import { useModule, useModuleFilter, useProject } from "@/hooks/store";
|
||||
// types
|
||||
|
||||
export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||
@ -16,6 +16,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { getFilteredModuleIds, moduleMap, updateModuleDetails } = useModule();
|
||||
const { currentProjectDisplayFilters: displayFilters } = useModuleFilter();
|
||||
// derived values
|
||||
const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined;
|
||||
|
||||
@ -54,7 +55,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={isAllowed}
|
||||
enableReorder={isAllowed && displayFilters?.order_by === "sort_order"}
|
||||
enableAddBlock={isAllowed}
|
||||
showAllBlocks
|
||||
/>
|
||||
|
@ -92,4 +92,8 @@ export const MODULE_ORDER_BY_OPTIONS: { key: TModuleOrderByOptions; label: strin
|
||||
key: "created_at",
|
||||
label: "Created date",
|
||||
},
|
||||
{
|
||||
key: "sort_order",
|
||||
label: "Manual",
|
||||
},
|
||||
];
|
||||
|
@ -9,7 +9,7 @@ import { satisfiesDateFilter } from "@/helpers/filter.helper";
|
||||
* @param {ICycle[]} cycles
|
||||
* @returns {ICycle[]}
|
||||
*/
|
||||
export const orderCycles = (cycles: ICycle[]): ICycle[] => {
|
||||
export const orderCycles = (cycles: ICycle[], sortByManual: boolean): ICycle[] => {
|
||||
if (cycles.length === 0) return [];
|
||||
|
||||
const acceptedStatuses = ["current", "upcoming", "draft"];
|
||||
@ -22,10 +22,12 @@ export const orderCycles = (cycles: ICycle[]): ICycle[] => {
|
||||
};
|
||||
|
||||
let filteredCycles = cycles.filter((c) => acceptedStatuses.includes(c.status?.toLowerCase() ?? ""));
|
||||
filteredCycles = sortBy(filteredCycles, [
|
||||
(c) => STATUS_ORDER[c.status?.toLowerCase() ?? ""],
|
||||
(c) => (c.status?.toLowerCase() === "upcoming" ? c.start_date : c.name.toLowerCase()),
|
||||
]);
|
||||
if (sortByManual) filteredCycles = sortBy(filteredCycles, [(c) => c.sort_order]);
|
||||
else
|
||||
filteredCycles = sortBy(filteredCycles, [
|
||||
(c) => STATUS_ORDER[c.status?.toLowerCase() ?? ""],
|
||||
(c) => (c.status?.toLowerCase() === "upcoming" ? c.start_date : c.name.toLowerCase()),
|
||||
]);
|
||||
|
||||
return filteredCycles;
|
||||
};
|
||||
|
@ -35,6 +35,7 @@ export const orderModules = (modules: IModule[], orderByKey: TModuleOrderByOptio
|
||||
if (orderByKey === "created_at") orderedModules = sortBy(modules, [(m) => m.created_at]);
|
||||
if (orderByKey === "-created_at") orderedModules = sortBy(modules, [(m) => !m.created_at]);
|
||||
|
||||
if (orderByKey === "sort_order") orderedModules = sortBy(modules, [(m) => m.sort_order]);
|
||||
return orderedModules;
|
||||
};
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.3",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.0.3",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.3.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@blueprintjs/popover2": "^1.13.3",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
@ -80,4 +80,4 @@
|
||||
"tsconfig": "*",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ export interface ICycleStore {
|
||||
currentProjectActiveCycleId: string | null;
|
||||
currentProjectArchivedCycleIds: string[] | null;
|
||||
// computed actions
|
||||
getFilteredCycleIds: (projectId: string) => string[] | null;
|
||||
getFilteredCycleIds: (projectId: string, sortByManual: boolean) => string[] | null;
|
||||
getFilteredCompletedCycleIds: (projectId: string) => string[] | null;
|
||||
getFilteredArchivedCycleIds: (projectId: string) => string[] | null;
|
||||
getCycleById: (cycleId: string) => ICycle | null;
|
||||
@ -228,7 +228,7 @@ export class CycleStore implements ICycleStore {
|
||||
* @param {TCycleFilters} filters
|
||||
* @returns {string[] | null}
|
||||
*/
|
||||
getFilteredCycleIds = computedFn((projectId: string) => {
|
||||
getFilteredCycleIds = computedFn((projectId: string, sortByManual: boolean) => {
|
||||
const filters = this.rootStore.cycleFilter.getFiltersByProjectId(projectId);
|
||||
const searchQuery = this.rootStore.cycleFilter.searchQuery;
|
||||
if (!this.fetchedMap[projectId]) return null;
|
||||
@ -239,7 +239,7 @@ export class CycleStore implements ICycleStore {
|
||||
c.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
|
||||
shouldFilterCycle(c, filters ?? {})
|
||||
);
|
||||
cycles = orderCycles(cycles);
|
||||
cycles = orderCycles(cycles, sortByManual);
|
||||
const cycleIds = cycles.map((c) => c.id);
|
||||
return cycleIds;
|
||||
});
|
||||
|
@ -636,4 +636,8 @@ div.web-view-spinner div.bar12 {
|
||||
/* highlight class */
|
||||
.highlight {
|
||||
border: 1px solid rgb(var(--color-primary-100)) !important;
|
||||
}
|
||||
.highlight-with-line {
|
||||
border-left: 5px solid rgb(var(--color-primary-100)) !important;
|
||||
background: rgb(var(--color-background-80));
|
||||
}
|
@ -29,10 +29,10 @@
|
||||
jsonpointer "^5.0.0"
|
||||
leven "^3.1.0"
|
||||
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@atlaskit/pragmatic-drag-and-drop-auto-scroll/-/pragmatic-drag-and-drop-auto-scroll-1.0.3.tgz#bb088a3d8eb77d9454dfdead433e594c5f793dae"
|
||||
integrity sha512-gfXwzQZRsWSLd/88IRNKwf2e5r3smZlDGLopg5tFkSqsL03VDHgd6wch6OfPhRK2kSCrT1uXv5n9hOpVBu678g==
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@atlaskit/pragmatic-drag-and-drop-auto-scroll/-/pragmatic-drag-and-drop-auto-scroll-1.3.0.tgz#6af382a2d75924f5f0699ebf1b348e2ea8d5a2cd"
|
||||
integrity sha512-8wjKAI5qSrLojt8ZJ2WhoS5P75oBu5g0yMpAnTDgfqFyQnkt5Uc1txCRWpG26SS1mv19nm8ak9XHF2DOugVfpw==
|
||||
dependencies:
|
||||
"@atlaskit/pragmatic-drag-and-drop" "^1.1.0"
|
||||
"@babel/runtime" "^7.0.0"
|
||||
|
Loading…
Reference in New Issue
Block a user