[WEB-1005] chore: pragmatic drag n drop implementation for labels (#4223)

* pragmatic drag n drop implementation for labels

* minor code quality improvements
This commit is contained in:
rahulramesha 2024-04-17 18:20:02 +05:30 committed by GitHub
parent 68c870b791
commit 10ed12e589
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 517 additions and 334 deletions

View file

@ -1,24 +1,25 @@
import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd";
import { forwardRef } from "react";
import { MoreVertical } from "lucide-react";
interface IDragHandle {
isDragging: boolean;
dragHandleProps: DraggableProvidedDragHandleProps;
}
export const DragHandle = (props: IDragHandle) => {
const { isDragging, dragHandleProps } = props;
export const DragHandle = forwardRef<HTMLButtonElement | null, IDragHandle>((props, ref) => {
const { isDragging } = props;
return (
<button
type="button"
className={`mr-1 flex flex-shrink-0 rounded text-custom-sidebar-text-200 group-hover:opacity-100 ${
className={`mr-1 flex flex-shrink-0 rounded text-custom-sidebar-text-200 group-hover:opacity-100 cursor-grab ${
isDragging ? "opacity-100" : "opacity-0"
}`}
{...dragHandleProps}
ref={ref}
>
<MoreVertical className="h-3.5 w-3.5 stroke-custom-text-400" />
<MoreVertical className="-ml-5 h-3.5 w-3.5 stroke-custom-text-400" />
</button>
);
};
});
DragHandle.displayName = "DragHandle";

View file

@ -1,5 +1,4 @@
import { useRef, useState } from "react";
import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd";
import { MutableRefObject, useRef, useState } from "react";
import { LucideIcon, X } from "lucide-react";
import { IIssueLabel } from "@plane/types";
//ui
@ -24,13 +23,13 @@ interface ILabelItemBlock {
label: IIssueLabel;
isDragging: boolean;
customMenuItems: ICustomMenuItem[];
dragHandleProps: DraggableProvidedDragHandleProps;
handleLabelDelete: (label: IIssueLabel) => void;
isLabelGroup?: boolean;
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
}
export const LabelItemBlock = (props: ILabelItemBlock) => {
const { label, isDragging, customMenuItems, dragHandleProps, handleLabelDelete, isLabelGroup } = props;
const { label, isDragging, customMenuItems, handleLabelDelete, isLabelGroup, dragHandleRef } = props;
// states
const [isMenuActive, setIsMenuActive] = useState(false);
// refs
@ -41,7 +40,7 @@ export const LabelItemBlock = (props: ILabelItemBlock) => {
return (
<div className="group flex items-center">
<div className="flex items-center">
<DragHandle isDragging={isDragging} dragHandleProps={dragHandleProps} />
<DragHandle isDragging={isDragging} ref={dragHandleRef} />
<LabelName color={label.color} name={label.name} isGroup={isLabelGroup ?? false} />
</div>

View file

@ -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 (
<div className="py-3 pl-2 pr-4 border-[1px] border-custom-border-200 bg-custom-background-100">
<LabelName name={label.name} color={label.color} isGroup={isGroup} />
</div>
);
};
type Props = {
label: IIssueLabel;
isGroup: boolean;
isChild: boolean;
isLastChild: boolean;
children: (
isDragging: boolean,
isDroppingInLabel: boolean,
dragHandleRef: MutableRefObject<HTMLButtonElement | null>
) => 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<InstructionType | undefined>(undefined);
// refs
const labelRef = useRef<HTMLDivElement | null>(null);
const dragHandleRef = useRef<HTMLButtonElement | null>(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(<LabelDragPreview label={label} isGroup={isGroup} />);
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 (
<div ref={labelRef}>
<DropIndicator classNames="my-1" isVisible={instruction === "reorder-above"} />
{children(isDragging, isMakeChild, dragHandleRef)}
{isLastChild && <DropIndicator classNames="my-1" isVisible={instruction === "reorder-below"} />}
</div>
);
});

View file

@ -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;
};

View file

@ -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<SetStateAction<boolean>>;
isDropDisabled: boolean;
isLastChild: boolean;
onDrop: (
draggingLabelId: string,
droppedParentId: string | null,
droppedLabelId: string | undefined,
dropAtEndOfList: boolean
) => void;
};
export const ProjectSettingLabelGroup: React.FC<Props> = 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<Props> = observer((props) => {
];
return (
<Disclosure
as="div"
className={`rounded border-[0.5px] border-custom-border-200 text-custom-text-100 ${
groupDragSnapshot.combineTargetFor ? "bg-custom-background-80" : "bg-custom-background-100"
}`}
defaultOpen
>
{({ open }) => (
<>
<Droppable
key={`label.group.droppable.${label.id}`}
droppableId={`label.group.droppable.${label.id}`}
isDropDisabled={groupDragSnapshot.isDragging || isUpdating || isDropDisabled}
<LabelDndHOC label={label} isGroup isChild={false} isLastChild={isLastChild} onDrop={onDrop}>
{(isDragging, isDroppingInLabel, dragHandleRef) => (
<div
className={`rounded ${isDroppingInLabel ? "border-[2px] border-custom-primary-100" : "border-[1.5px] border-transparent"}`}
>
<Disclosure
as="div"
className={`rounded text-custom-text-100 ${
!isDroppingInLabel ? "border-[0.5px] border-custom-border-200" : ""
} ${isDragging ? "bg-custom-background-80" : "bg-custom-background-100"}`}
defaultOpen
>
{(droppableProvided) => (
<div
className={`py-3 pl-1 pr-3 ${!isUpdating && "max-h-full overflow-y-hidden"}`}
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<>
<div className="relative flex cursor-pointer items-center justify-between gap-2">
{isEditLabelForm ? (
<CreateUpdateLabelInline
labelForm={isEditLabelForm}
setLabelForm={setEditLabelForm}
isUpdating
labelToUpdate={label}
onClose={() => {
setEditLabelForm(false);
setIsUpdating(false);
}}
/>
) : (
<LabelItemBlock
label={label}
isDragging={groupDragSnapshot.isDragging}
customMenuItems={customMenuItems}
dragHandleProps={dragHandleProps}
handleLabelDelete={handleLabelDelete}
isLabelGroup
/>
)}
<Disclosure.Button>
<span>
<ChevronDown
className={`h-4 w-4 text-custom-sidebar-text-400 ${!open ? "rotate-90 transform" : ""}`}
{({ open }) => (
<>
<div className={`py-3 pl-1 pr-3 ${!isUpdating && "max-h-full overflow-y-hidden"}`}>
<>
<div className="relative flex cursor-pointer items-center justify-between gap-2">
{isEditLabelForm ? (
<CreateUpdateLabelInline
labelForm={isEditLabelForm}
setLabelForm={setEditLabelForm}
isUpdating
labelToUpdate={label}
onClose={() => {
setEditLabelForm(false);
setIsUpdating(false);
}}
/>
</span>
</Disclosure.Button>
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="ml-6 mt-2.5">
{labelChildren.map((child, index) => (
<div key={child.id} className={`group flex w-full items-center text-sm`}>
<Draggable
draggableId={`label.draggable.${child.id}`}
index={index}
isDragDisabled={groupDragSnapshot.isDragging || isUpdating}
>
{renderDraggable((provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
<div className="w-full py-1" ref={provided.innerRef} {...provided.draggableProps}>
<ProjectSettingLabelItem
label={child}
handleLabelDelete={() => handleLabelDelete(child)}
draggableSnapshot={snapshot}
dragHandleProps={provided.dragHandleProps!}
setIsUpdating={setIsUpdating}
isChild
/>
</div>
))}
</Draggable>
</div>
))}
</div>
</Disclosure.Panel>
</Transition>
{droppableProvided.placeholder}
</>
</div>
) : (
<LabelItemBlock
label={label}
isDragging={isDragging}
customMenuItems={customMenuItems}
handleLabelDelete={handleLabelDelete}
isLabelGroup
dragHandleRef={dragHandleRef}
/>
)}
<Disclosure.Button>
<span>
<ChevronDown
className={`h-4 w-4 text-custom-sidebar-text-400 ${!open ? "rotate-90 transform" : ""}`}
/>
</span>
</Disclosure.Button>
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="ml-6">
{labelChildren.map((child, index) => (
<div key={child.id} className={`group flex w-full items-center text-sm`}>
<div className="w-full">
<ProjectSettingLabelItem
label={child}
handleLabelDelete={() => handleLabelDelete(child)}
setIsUpdating={setIsUpdating}
isParentDragging={isDragging}
isChild
isLastChild={index === labelChildren.length - 1}
onDrop={onDrop}
/>
</div>
</div>
))}
</div>
</Disclosure.Panel>
</Transition>
</>
</div>
</>
)}
</Droppable>
</>
</Disclosure>
</div>
)}
</Disclosure>
</LabelDndHOC>
);
});

View file

@ -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<SetStateAction<boolean>>;
isParentDragging?: boolean;
isChild: boolean;
isLastChild: boolean;
onDrop: (
draggingLabelId: string,
droppedParentId: string | null,
droppedLabelId: string | undefined,
dropAtEndOfList: boolean
) => void;
};
export const ProjectSettingLabelItem: React.FC<Props> = (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> = (props) => {
];
return (
<div
className={`group relative flex items-center justify-between gap-2 space-y-3 rounded border-[0.5px] border-custom-border-200 ${
!isChild && combineTargetFor ? "bg-custom-background-80" : ""
} ${isDragging ? "bg-custom-background-80 shadow-custom-shadow-xs" : ""} bg-custom-background-100 px-1 py-2.5`}
>
{isEditLabelForm ? (
<CreateUpdateLabelInline
labelForm={isEditLabelForm}
setLabelForm={setEditLabelForm}
isUpdating
labelToUpdate={label}
onClose={() => {
setEditLabelForm(false);
setIsUpdating(false);
}}
/>
) : (
<LabelItemBlock
label={label}
isDragging={isDragging}
customMenuItems={customMenuItems}
dragHandleProps={dragHandleProps}
handleLabelDelete={handleLabelDelete}
/>
<LabelDndHOC label={label} isGroup={false} isChild={isChild} isLastChild={isLastChild} onDrop={onDrop}>
{(isDragging, isDroppingInLabel, dragHandleRef) => (
<div
className={`rounded ${isDroppingInLabel ? "border-[2px] border-custom-primary-100" : "border-[1.5px] border-transparent"}`}
>
<div
className={`py-3 px-1 group relative flex items-center justify-between gap-2 space-y-3 rounded ${
isDroppingInLabel ? "" : "border-[0.5px] border-custom-border-200"
} ${isDragging || isParentDragging ? "bg-custom-background-80" : "bg-custom-background-100"}`}
>
{isEditLabelForm ? (
<CreateUpdateLabelInline
labelForm={isEditLabelForm}
setLabelForm={setEditLabelForm}
isUpdating
labelToUpdate={label}
onClose={() => {
setEditLabelForm(false);
setIsUpdating(false);
}}
/>
) : (
<LabelItemBlock
label={label}
isDragging={isDragging}
customMenuItems={customMenuItems}
handleLabelDelete={handleLabelDelete}
dragHandleRef={dragHandleRef}
/>
)}
</div>
</div>
)}
</div>
</LabelDndHOC>
);
};

View file

@ -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<IIssueLabel | null>(null);
const [isDraggingGroup, setIsDraggingGroup] = useState(false);
// refs
const scrollToRef = useRef<HTMLFormElement>(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(() => {
</div>
) : (
projectLabelsTree && (
<DragDropContext
onDragEnd={onDragEnd}
autoScrollerOptions={{
startFromPercentage: 1,
disabled: false,
maxScrollAtPercentage: 0,
maxPixelScroll: 2,
}}
>
<Droppable
droppableId={LABELS_ROOT}
isCombineEnabled={!isDraggingGroup}
ignoreContainerClipping
isDropDisabled={isUpdating}
>
{(droppableProvided, droppableSnapshot) => (
<div className="mt-3" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
{projectLabelsTree.map((label, index) => {
if (label.children && label.children.length) {
return (
<Draggable
key={`label.draggable.${label.id}`}
draggableId={`label.draggable.${label.id}.group`}
index={index}
isDragDisabled={isUpdating}
>
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => {
const isGroup = droppableSnapshot.draggingFromThisWith?.split(".")[3] === "group";
setIsDraggingGroup(isGroup);
return (
<div ref={provided.innerRef} {...provided.draggableProps} className="mt-3">
<ProjectSettingLabelGroup
key={label.id}
label={label}
labelChildren={label.children || []}
isDropDisabled={isGroup}
dragHandleProps={provided.dragHandleProps!}
handleLabelDelete={(label: IIssueLabel) => setSelectDeleteLabel(label)}
draggableSnapshot={snapshot}
isUpdating={isUpdating}
setIsUpdating={setIsUpdating}
/>
</div>
);
}}
</Draggable>
);
}
return (
<Draggable
key={`label.draggable.${label.id}`}
draggableId={`label.draggable.${label.id}`}
index={index}
isDragDisabled={isUpdating}
>
{renderDraggable((provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps} className="mt-3">
<ProjectSettingLabelItem
dragHandleProps={provided.dragHandleProps!}
draggableSnapshot={snapshot}
label={label}
setIsUpdating={setIsUpdating}
handleLabelDelete={(label) => setSelectDeleteLabel(label)}
isChild={false}
/>
</div>
))}
</Draggable>
);
})}
{droppableProvided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
<div className="mt-3">
{projectLabelsTree.map((label, index) => {
if (label.children && label.children.length) {
return (
<ProjectSettingLabelGroup
key={label.id}
label={label}
labelChildren={label.children || []}
handleLabelDelete={(label: IIssueLabel) => setSelectDeleteLabel(label)}
isUpdating={isUpdating}
setIsUpdating={setIsUpdating}
isLastChild={index === projectLabelsTree.length - 1}
onDrop={onDrop}
/>
);
}
return (
<ProjectSettingLabelItem
label={label}
key={label.id}
setIsUpdating={setIsUpdating}
handleLabelDelete={(label) => setSelectDeleteLabel(label)}
isChild={false}
isLastChild={index === projectLabelsTree.length - 1}
onDrop={onDrop}
/>
);
})}
</div>
)
)
) : (