-
+
diff --git a/web/components/labels/label-drag-n-drop-HOC.tsx b/web/components/labels/label-drag-n-drop-HOC.tsx
new file mode 100644
index 000000000..3898d6b7a
--- /dev/null
+++ b/web/components/labels/label-drag-n-drop-HOC.tsx
@@ -0,0 +1,161 @@
+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 { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
+import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
+import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
+import { observer } from "mobx-react";
+import { createRoot } from "react-dom/client";
+// types
+import { IIssueLabel, InstructionType } from "@plane/types";
+// ui
+import { DropIndicator } from "@plane/ui";
+// components
+import { LabelName } from "./label-block/label-name";
+import { TargetData, getCanDrop, getInstructionFromPayload } from "./label-utils";
+
+type LabelDragPreviewProps = {
+ label: IIssueLabel;
+ isGroup: boolean;
+};
+
+export const LabelDragPreview = (props: LabelDragPreviewProps) => {
+ const { label, isGroup } = props;
+
+ return (
+
+
+
+ );
+};
+
+type Props = {
+ label: IIssueLabel;
+ isGroup: boolean;
+ isChild: boolean;
+ isLastChild: boolean;
+ children: (
+ isDragging: boolean,
+ isDroppingInLabel: boolean,
+ dragHandleRef: MutableRefObject
+ ) => JSX.Element;
+ onDrop: (
+ draggingLabelId: string,
+ droppedParentId: string | null,
+ droppedLabelId: string | undefined,
+ dropAtEndOfList: boolean
+ ) => void;
+};
+
+export const LabelDndHOC = observer((props: Props) => {
+ const { label, isGroup, isChild, isLastChild, children, onDrop } = props;
+
+ const [isDragging, setIsDragging] = useState(false);
+ const [instruction, setInstruction] = useState(undefined);
+ // refs
+ const labelRef = useRef(null);
+ const dragHandleRef = useRef(null);
+
+ useEffect(() => {
+ const element = labelRef.current;
+ const dragHandleElement = dragHandleRef.current;
+
+ if (!element) return;
+
+ return combine(
+ draggable({
+ element,
+ dragHandle: dragHandleElement ?? undefined,
+ getInitialData: () => ({ id: label?.id, parentId: label?.parent, isGroup, isChild }),
+ onDragStart: () => {
+ setIsDragging(true);
+ },
+ onDrop: () => {
+ setIsDragging(false);
+ },
+ onGenerateDragPreview: ({ nativeSetDragImage }) => {
+ setCustomNativeDragPreview({
+ getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
+ render: ({ container }) => {
+ const root = createRoot(container);
+ root.render();
+ return () => root.unmount();
+ },
+ nativeSetDragImage,
+ });
+ },
+ }),
+ dropTargetForElements({
+ element,
+ canDrop: ({ source }) => getCanDrop(source, label, isChild),
+ getData: ({ input, element }) => {
+ const data = { id: label?.id, parentId: label?.parent, isGroup, isChild };
+
+ const blockedStates: InstructionType[] = [];
+
+ // if is currently a child then block make-child instruction
+ if (isChild) blockedStates.push("make-child");
+ // if is currently is not a last child then block reorder-below instruction
+ if (!isLastChild) blockedStates.push("reorder-below");
+
+ return attachInstruction(data, {
+ input,
+ element,
+ currentLevel: isChild ? 1 : 0,
+ indentPerLevel: 0,
+ mode: isLastChild ? "last-in-group" : "standard",
+ block: blockedStates,
+ });
+ },
+ onDrag: ({ self, source, location }) => {
+ const instruction = getInstructionFromPayload(self, source, location);
+ setInstruction(instruction);
+ },
+ onDragLeave: () => {
+ setInstruction(undefined);
+ },
+ onDrop: ({ source, location }) => {
+ setInstruction(undefined);
+
+ const dropTargets = location?.current?.dropTargets ?? [];
+
+ if (isChild || !dropTargets || dropTargets.length <= 0) return;
+
+ // if the label is dropped on both a child and it's parent at the same time then get only the child's drop target
+ const dropTarget =
+ dropTargets.length > 1 ? dropTargets.find((target) => target?.data?.isChild) : dropTargets[0];
+
+ let parentId: string | null = null,
+ dropAtEndOfList = false;
+
+ const dropTargetData = dropTarget?.data as TargetData;
+
+ if (!dropTarget || !dropTargetData) return;
+
+ // get possible instructions for the dropTarget
+ const instruction = getInstructionFromPayload(dropTarget, source, location);
+
+ // if instruction is make child the set parentId as current dropTarget Id or else set it as dropTarget's parentId
+ parentId = instruction === "make-child" ? dropTargetData.id : dropTargetData.parentId;
+ // if instruction is any other than make-child, i.e., reorder-above and reorder-below then set the droppedId as dropTarget's id
+ const droppedLabelId = instruction !== "make-child" ? dropTargetData.id : undefined;
+ // if instruction is to reorder-below that is enabled only for end of the last items in the list then dropAtEndOfList as true
+ if (instruction === "reorder-below") dropAtEndOfList = true;
+
+ const sourceData = source.data as TargetData;
+ if (sourceData.id) onDrop(sourceData.id as string, parentId, droppedLabelId, dropAtEndOfList);
+ },
+ })
+ );
+ }, [labelRef?.current, dragHandleRef?.current, label, isChild, isGroup, isLastChild, onDrop]);
+
+ const isMakeChild = instruction == "make-child";
+
+ return (
+
+
+ {children(isDragging, isMakeChild, dragHandleRef)}
+ {isLastChild && }
+
+ );
+});
diff --git a/web/components/labels/label-utils.ts b/web/components/labels/label-utils.ts
new file mode 100644
index 000000000..1919e53f3
--- /dev/null
+++ b/web/components/labels/label-utils.ts
@@ -0,0 +1,67 @@
+import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
+import { IIssueLabel, IPragmaticPayloadLocation, InstructionType, TDropTarget } from "@plane/types";
+
+export type TargetData = {
+ id: string;
+ parentId: string | null;
+ isGroup: boolean;
+ isChild: boolean;
+};
+
+/**
+ * extracts the Payload and translates the instruction for the current dropTarget based on drag and drop payload
+ * @param dropTarget dropTarget for which the instruction is required
+ * @param source the dragging label data that is being dragged on the dropTarget
+ * @param location location includes the data of all the dropTargets the source is being dragged on
+ * @returns Instruction for dropTarget
+ */
+export const getInstructionFromPayload = (
+ dropTarget: TDropTarget,
+ source: TDropTarget,
+ location: IPragmaticPayloadLocation
+): InstructionType | undefined => {
+ const dropTargetData = dropTarget?.data as TargetData;
+ const sourceData = source?.data as TargetData;
+
+ const allDropTargets = location?.current?.dropTargets;
+
+ // if all the dropTargets are greater than 1 meaning the source is being dragged on a group and its child at the same time
+ // and also if the dropTarget in question is also a group then, it should be a child of the current Droptarget
+ if (allDropTargets?.length > 1 && dropTargetData?.isGroup) return "make-child";
+
+ if (!dropTargetData || !sourceData) return undefined;
+
+ let instruction = extractInstruction(dropTargetData)?.type;
+
+ // If the instruction is blocked then set an instruction based on if dropTarget it is a child or not
+ if (instruction === "instruction-blocked") {
+ instruction = dropTargetData.isChild ? "reorder-above" : "make-child";
+ }
+
+ // if source that is being dragged is a group. A group cannon be a child of any other label,
+ // hence if current instruction is to be a child of dropTarget then reorder-above instead
+ if (instruction === "make-child" && sourceData.isGroup) instruction = "reorder-above";
+
+ return instruction;
+};
+
+/**
+ * This provides a boolean to indicate if the label can be dropped onto the droptarget
+ * @param source
+ * @param label
+ * @param isCurrentChild if the dropTarget is a child
+ * @returns
+ */
+export const getCanDrop = (source: TDropTarget, label: IIssueLabel | undefined, isCurrentChild: boolean) => {
+ const sourceData = source?.data;
+
+ if (!sourceData) return false;
+
+ // a label cannot be dropped on to itself and it's parent cannon be dropped on the child
+ if (sourceData.id === label?.id || sourceData.id === label?.parent) return false;
+
+ // if current dropTarget is a child and the label being dropped is a group then don't enable drop
+ if (isCurrentChild && sourceData.isGroup) return false;
+
+ return true;
+};
diff --git a/web/components/labels/project-setting-label-group.tsx b/web/components/labels/project-setting-label-group.tsx
index 0a3efc9df..3302cb6bf 100644
--- a/web/components/labels/project-setting-label-group.tsx
+++ b/web/components/labels/project-setting-label-group.tsx
@@ -1,51 +1,38 @@
import React, { Dispatch, SetStateAction, useState } from "react";
-import {
- Draggable,
- DraggableProvided,
- DraggableProvidedDragHandleProps,
- DraggableStateSnapshot,
- Droppable,
-} from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
import { ChevronDown, Pencil, Trash2 } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
-
// store
// icons
-import { IIssueLabel } from "@plane/types";
// types
-import useDraggableInPortal from "@/hooks/use-draggable-portal";
+import { IIssueLabel } from "@plane/types";
+// components
import { CreateUpdateLabelInline } from "./create-update-label-inline";
import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
+import { LabelDndHOC } from "./label-drag-n-drop-HOC";
import { ProjectSettingLabelItem } from "./project-setting-label-item";
type Props = {
label: IIssueLabel;
labelChildren: IIssueLabel[];
handleLabelDelete: (label: IIssueLabel) => void;
- dragHandleProps: DraggableProvidedDragHandleProps;
- draggableSnapshot: DraggableStateSnapshot;
isUpdating: boolean;
setIsUpdating: Dispatch>;
- isDropDisabled: boolean;
+ isLastChild: boolean;
+ onDrop: (
+ draggingLabelId: string,
+ droppedParentId: string | null,
+ droppedLabelId: string | undefined,
+ dropAtEndOfList: boolean
+ ) => void;
};
export const ProjectSettingLabelGroup: React.FC = observer((props) => {
- const {
- label,
- labelChildren,
- handleLabelDelete,
- draggableSnapshot: groupDragSnapshot,
- dragHandleProps,
- isUpdating,
- setIsUpdating,
- isDropDisabled,
- } = props;
+ const { label, labelChildren, handleLabelDelete, isUpdating, setIsUpdating, isLastChild, onDrop } = props;
+ // states
const [isEditLabelForm, setEditLabelForm] = useState(false);
- const renderDraggable = useDraggableInPortal();
-
const customMenuItems: ICustomMenuItem[] = [
{
CustomIcon: Pencil,
@@ -67,101 +54,89 @@ export const ProjectSettingLabelGroup: React.FC = observer((props) => {
];
return (
-
- {({ open }) => (
- <>
-
+ {(isDragging, isDroppingInLabel, dragHandleRef) => (
+
+
- {(droppableProvided) => (
-
- <>
-
- {isEditLabelForm ? (
-
{
- setEditLabelForm(false);
- setIsUpdating(false);
- }}
- />
- ) : (
-
- )}
-
-
-
- (
+ <>
+
+ <>
+
+ {isEditLabelForm ? (
+ {
+ setEditLabelForm(false);
+ setIsUpdating(false);
+ }}
/>
-
-
-
-
-
-
- {labelChildren.map((child, index) => (
-
-
- {renderDraggable((provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
-
-
handleLabelDelete(child)}
- draggableSnapshot={snapshot}
- dragHandleProps={provided.dragHandleProps!}
- setIsUpdating={setIsUpdating}
- isChild
- />
-
- ))}
-
-
- ))}
-
-
-
- {droppableProvided.placeholder}
- >
-
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {labelChildren.map((child, index) => (
+
+
+
handleLabelDelete(child)}
+ setIsUpdating={setIsUpdating}
+ isParentDragging={isDragging}
+ isChild
+ isLastChild={index === labelChildren.length - 1}
+ onDrop={onDrop}
+ />
+
+
+ ))}
+
+
+
+ >
+
+ >
)}
-
- >
+
+
)}
-
+
);
});
diff --git a/web/components/labels/project-setting-label-item.tsx b/web/components/labels/project-setting-label-item.tsx
index d8bbee719..0a596fe41 100644
--- a/web/components/labels/project-setting-label-item.tsx
+++ b/web/components/labels/project-setting-label-item.tsx
@@ -1,27 +1,32 @@
import React, { Dispatch, SetStateAction, useState } from "react";
-import { DraggableProvidedDragHandleProps, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { useRouter } from "next/router";
import { X, Pencil } from "lucide-react";
+// types
import { IIssueLabel } from "@plane/types";
// hooks
import { useLabel } from "@/hooks/store";
-// types
// components
import { CreateUpdateLabelInline } from "./create-update-label-inline";
import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
+import { LabelDndHOC } from "./label-drag-n-drop-HOC";
type Props = {
label: IIssueLabel;
handleLabelDelete: (label: IIssueLabel) => void;
- draggableSnapshot: DraggableStateSnapshot;
- dragHandleProps: DraggableProvidedDragHandleProps;
setIsUpdating: Dispatch>;
+ isParentDragging?: boolean;
isChild: boolean;
+ isLastChild: boolean;
+ onDrop: (
+ draggingLabelId: string,
+ droppedParentId: string | null,
+ droppedLabelId: string | undefined,
+ dropAtEndOfList: boolean
+ ) => void;
};
export const ProjectSettingLabelItem: React.FC = (props) => {
- const { label, setIsUpdating, handleLabelDelete, draggableSnapshot, dragHandleProps, isChild } = props;
- const { combineTargetFor, isDragging } = draggableSnapshot;
+ const { label, setIsUpdating, handleLabelDelete, isChild, isLastChild, isParentDragging = false, onDrop } = props;
// states
const [isEditLabelForm, setEditLabelForm] = useState(false);
// router
@@ -59,31 +64,39 @@ export const ProjectSettingLabelItem: React.FC = (props) => {
];
return (
-
- {isEditLabelForm ? (
-
{
- setEditLabelForm(false);
- setIsUpdating(false);
- }}
- />
- ) : (
-
+
+ {(isDragging, isDroppingInLabel, dragHandleRef) => (
+
+
+ {isEditLabelForm ? (
+ {
+ setEditLabelForm(false);
+ setIsUpdating(false);
+ }}
+ />
+ ) : (
+
+ )}
+
+
)}
-
+
);
};
diff --git a/web/components/labels/project-setting-label-list.tsx b/web/components/labels/project-setting-label-list.tsx
index bbc4023d1..8de57e8b2 100644
--- a/web/components/labels/project-setting-label-list.tsx
+++ b/web/components/labels/project-setting-label-list.tsx
@@ -1,12 +1,4 @@
import React, { useState, useRef } from "react";
-import {
- DragDropContext,
- Draggable,
- DraggableProvided,
- DraggableStateSnapshot,
- DropResult,
- Droppable,
-} from "@hello-pangea/dnd";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { IIssueLabel } from "@plane/types";
@@ -21,20 +13,16 @@ import {
} from "@/components/labels";
import { EmptyStateType } from "@/constants/empty-state";
import { useLabel } from "@/hooks/store";
-import useDraggableInPortal from "@/hooks/use-draggable-portal";
// components
// ui
// types
// constants
-const LABELS_ROOT = "labels.root";
-
export const ProjectSettingsLabelList: React.FC = observer(() => {
// states
const [showLabelForm, setLabelForm] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [selectDeleteLabel, setSelectDeleteLabel] = useState(null);
- const [isDraggingGroup, setIsDraggingGroup] = useState(false);
// refs
const scrollToRef = useRef(null);
// router
@@ -42,44 +30,28 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
const { workspaceSlug, projectId } = router.query;
// store hooks
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
- // portal
- const renderDraggable = useDraggableInPortal();
const newLabel = () => {
setIsUpdating(false);
setLabelForm(true);
};
- const onDragEnd = (result: DropResult) => {
- const { combine, draggableId, destination, source } = result;
-
- // return if dropped outside the DragDropContext
- if (!combine && !destination) return;
-
- const childLabel = draggableId.split(".")[2];
- let parentLabel: string | undefined | null = destination?.droppableId?.split(".")[3];
- const index = destination?.index || 0;
-
- const prevParentLabel: string | undefined | null = source?.droppableId?.split(".")[3];
- const prevIndex = source?.index;
-
- if (combine && combine.draggableId) parentLabel = combine?.draggableId?.split(".")[2];
-
- if (destination?.droppableId === LABELS_ROOT) parentLabel = null;
-
- if (result.reason == "DROP" && childLabel != parentLabel) {
- if (workspaceSlug && projectId) {
- updateLabelPosition(
- workspaceSlug?.toString(),
- projectId?.toString(),
- childLabel,
- parentLabel,
- index,
- prevParentLabel == parentLabel,
- prevIndex
- );
- return;
- }
+ const onDrop = (
+ draggingLabelId: string,
+ droppedParentId: string | null,
+ droppedLabelId: string | undefined,
+ dropAtEndOfList: boolean
+ ) => {
+ if (workspaceSlug && projectId) {
+ updateLabelPosition(
+ workspaceSlug?.toString(),
+ projectId?.toString(),
+ draggingLabelId,
+ droppedParentId,
+ droppedLabelId,
+ dropAtEndOfList
+ );
+ return;
}
};
@@ -118,82 +90,35 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
) : (
projectLabelsTree && (
-