mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[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:
parent
68c870b791
commit
10ed12e589
31
packages/types/src/pragmatic.d.ts
vendored
31
packages/types/src/pragmatic.d.ts
vendored
@ -8,18 +8,27 @@ export type TDropTargetMiscellaneousData = {
|
||||
isActiveDueToStickiness: boolean;
|
||||
};
|
||||
|
||||
export interface IPragmaticDropPayload {
|
||||
location: {
|
||||
initial: {
|
||||
dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
|
||||
};
|
||||
current: {
|
||||
dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
|
||||
};
|
||||
previous: {
|
||||
dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
|
||||
};
|
||||
export interface IPragmaticPayloadLocation {
|
||||
initial: {
|
||||
dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
|
||||
};
|
||||
current: {
|
||||
dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
|
||||
};
|
||||
previous: {
|
||||
dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IPragmaticDropPayload {
|
||||
location: IPragmaticPayloadLocation;
|
||||
source: TDropTarget;
|
||||
self: TDropTarget & TDropTargetMiscellaneousData;
|
||||
}
|
||||
|
||||
export type InstructionType =
|
||||
| "reparent"
|
||||
| "reorder-above"
|
||||
| "reorder-below"
|
||||
| "make-child"
|
||||
| "instruction-blocked";
|
@ -3,9 +3,12 @@ import { cn } from "../helpers";
|
||||
|
||||
type Props = {
|
||||
isVisible: boolean;
|
||||
classNames?: string;
|
||||
};
|
||||
|
||||
export const DropIndicator = (props: Props) => {
|
||||
const { isVisible, classNames = "" } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -13,8 +16,9 @@ export const DropIndicator = (props: Props) => {
|
||||
before:left-0 before:relative before:block before:top-[-2px] before:h-[6px] before:w-[6px] before:rounded
|
||||
after:left-[calc(100%-6px)] after:relative after:block after:top-[-8px] after:h-[6px] after:w-[6px] after:rounded`,
|
||||
{
|
||||
"bg-custom-primary-100 before:bg-custom-primary-100 after:bg-custom-primary-100": props.isVisible,
|
||||
}
|
||||
"bg-custom-primary-100 before:bg-custom-primary-100 after:bg-custom-primary-100": isVisible,
|
||||
},
|
||||
classNames
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
|
||||
|
161
web/components/labels/label-drag-n-drop-HOC.tsx
Normal file
161
web/components/labels/label-drag-n-drop-HOC.tsx
Normal 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>
|
||||
);
|
||||
});
|
67
web/components/labels/label-utils.ts
Normal file
67
web/components/labels/label-utils.ts
Normal 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;
|
||||
};
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IIssueLabelTree } from "@plane/types";
|
||||
import { IIssueLabel, IIssueLabelTree } from "@plane/types";
|
||||
|
||||
export const groupBy = (array: any[], key: string) => {
|
||||
const innerKey = key.split("."); // split the key by dot
|
||||
@ -77,7 +77,7 @@ export const orderGroupedDataByField = <T>(groupedData: GroupedItems<T>, orderBy
|
||||
return groupedData;
|
||||
};
|
||||
|
||||
export const buildTree = (array: any[], parent = null) => {
|
||||
export const buildTree = (array: IIssueLabel[], parent = null) => {
|
||||
const tree: IIssueLabelTree[] = [];
|
||||
|
||||
array.forEach((item: any) => {
|
||||
|
@ -14,6 +14,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-hitbox": "^1.0.3",
|
||||
"@blueprintjs/popover2": "^1.13.3",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@hello-pangea/dnd": "^16.3.0",
|
||||
@ -63,7 +64,6 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.2.5",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/lodash": "^4.14.202",
|
||||
@ -74,6 +74,7 @@
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"eslint-config-custom": "*",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsconfig": "*",
|
||||
"typescript": "4.7.4"
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { ReactElement } from "react";
|
||||
import { ReactElement, 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";
|
||||
// layouts
|
||||
import { PageHead } from "@/components/core";
|
||||
@ -16,10 +18,25 @@ const LabelsSettingsPage: NextPageWithLayout = observer(() => {
|
||||
const { currentProjectDetails } = useProject();
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined;
|
||||
|
||||
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// Enable Auto Scroll for Labels list
|
||||
useEffect(() => {
|
||||
const element = scrollableContainerRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
autoScrollForElements({
|
||||
element,
|
||||
})
|
||||
);
|
||||
}, [scrollableContainerRef?.current]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="h-full w-full gap-10 overflow-y-auto py-8 pr-9">
|
||||
<div ref={scrollableContainerRef} className="h-full w-full gap-10 overflow-y-auto py-8 pr-9">
|
||||
<ProjectSettingsLabelList />
|
||||
</div>
|
||||
</>
|
||||
|
@ -36,12 +36,11 @@ export interface ILabelStore {
|
||||
updateLabelPosition: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
labelId: string,
|
||||
parentId: string | null | undefined,
|
||||
index: number,
|
||||
isSameParent: boolean,
|
||||
prevIndex: number | undefined
|
||||
) => Promise<IIssueLabel | undefined>;
|
||||
draggingLabelId: string,
|
||||
droppedParentId: string | null,
|
||||
droppedLabelId: string | undefined,
|
||||
dropAtEndOfList: boolean
|
||||
) => Promise<void>;
|
||||
deleteLabel: (workspaceSlug: string, projectId: string, labelId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@ -81,8 +80,8 @@ export class LabelStore implements ILabelStore {
|
||||
*/
|
||||
get workspaceLabels() {
|
||||
const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
|
||||
const worksapceSlug = this.rootStore.app.router.workspaceSlug || "";
|
||||
if (!currentWorkspaceDetails || !this.fetchedMap[worksapceSlug]) return;
|
||||
const workspaceSlug = this.rootStore.app.router.workspaceSlug || "";
|
||||
if (!currentWorkspaceDetails || !this.fetchedMap[workspaceSlug]) return;
|
||||
return sortBy(
|
||||
Object.values(this.labelMap).filter((label) => label.workspace_id === currentWorkspaceDetails.id),
|
||||
"sort_order"
|
||||
@ -94,8 +93,8 @@ export class LabelStore implements ILabelStore {
|
||||
*/
|
||||
get projectLabels() {
|
||||
const projectId = this.rootStore.app.router.projectId;
|
||||
const worksapceSlug = this.rootStore.app.router.workspaceSlug || "";
|
||||
if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return;
|
||||
const workspaceSlug = this.rootStore.app.router.workspaceSlug || "";
|
||||
if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[workspaceSlug])) return;
|
||||
return sortBy(
|
||||
Object.values(this.labelMap).filter((label) => label.project_id === projectId),
|
||||
"sort_order"
|
||||
@ -111,8 +110,8 @@ export class LabelStore implements ILabelStore {
|
||||
}
|
||||
|
||||
getProjectLabels = computedFn((projectId: string | null) => {
|
||||
const worksapceSlug = this.rootStore.app.router.workspaceSlug || "";
|
||||
if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return;
|
||||
const workspaceSlug = this.rootStore.app.router.workspaceSlug || "";
|
||||
if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[workspaceSlug])) return;
|
||||
return sortBy(
|
||||
Object.values(this.labelMap).filter((label) => label.project_id === projectId),
|
||||
"sort_order"
|
||||
@ -186,7 +185,7 @@ export class LabelStore implements ILabelStore {
|
||||
const originalLabel = this.labelMap[labelId];
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.labelMap, [labelId], { ...this.labelMap[labelId], ...data });
|
||||
set(this.labelMap, [labelId], { ...originalLabel, ...data });
|
||||
});
|
||||
const response = await this.issueLabelService.patchIssueLabel(workspaceSlug, projectId, labelId, data);
|
||||
return response;
|
||||
@ -213,50 +212,54 @@ export class LabelStore implements ILabelStore {
|
||||
updateLabelPosition = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
labelId: string,
|
||||
parentId: string | null | undefined,
|
||||
index: number,
|
||||
isSameParent: boolean,
|
||||
prevIndex: number | undefined
|
||||
draggingLabelId: string,
|
||||
droppedParentId: string | null,
|
||||
droppedLabelId: string | undefined,
|
||||
dropAtEndOfList: boolean
|
||||
) => {
|
||||
const currLabel = this.labelMap?.[labelId];
|
||||
const currLabel = this.labelMap?.[draggingLabelId];
|
||||
const labelTree = this.projectLabelsTree;
|
||||
let currentArray: IIssueLabel[];
|
||||
|
||||
if (!currLabel || !labelTree) return;
|
||||
|
||||
const data: Partial<IIssueLabel> = { parent: parentId };
|
||||
//find array in which the label is to be added
|
||||
if (!parentId) currentArray = labelTree;
|
||||
else currentArray = labelTree?.find((label) => label.id === parentId)?.children || [];
|
||||
//If its is dropped in the same parent then, there is not specific label on which it is mentioned then keep it's original position
|
||||
if (currLabel.parent === droppedParentId && !droppedLabelId) return;
|
||||
|
||||
//Add the array at the destination
|
||||
if (isSameParent && prevIndex !== undefined) currentArray.splice(prevIndex, 1);
|
||||
currentArray.splice(index, 0, currLabel);
|
||||
const data: Partial<IIssueLabel> = { parent: droppedParentId };
|
||||
|
||||
// find array in which the label is to be added
|
||||
if (!droppedParentId) currentArray = labelTree;
|
||||
else currentArray = labelTree?.find((label) => label.id === droppedParentId)?.children || [];
|
||||
|
||||
let droppedLabelIndex = currentArray.findIndex((label) => label.id === droppedLabelId);
|
||||
//if the position of droppedLabelId cannot be determined then drop it at the end of the list
|
||||
if (dropAtEndOfList || droppedLabelIndex === -1) droppedLabelIndex = currentArray.length;
|
||||
|
||||
//if currently adding to a new array, then let backend assign a sort order
|
||||
if (currentArray.length > 1) {
|
||||
if (currentArray.length > 0) {
|
||||
let prevSortOrder: number | undefined, nextSortOrder: number | undefined;
|
||||
|
||||
if (typeof currentArray[index - 1] !== "undefined") {
|
||||
prevSortOrder = currentArray[index - 1].sort_order;
|
||||
if (typeof currentArray[droppedLabelIndex - 1] !== "undefined") {
|
||||
prevSortOrder = currentArray[droppedLabelIndex - 1].sort_order;
|
||||
}
|
||||
if (typeof currentArray[index + 1] !== "undefined") {
|
||||
nextSortOrder = currentArray[index + 1].sort_order;
|
||||
if (typeof currentArray[droppedLabelIndex] !== "undefined") {
|
||||
nextSortOrder = currentArray[droppedLabelIndex].sort_order;
|
||||
}
|
||||
|
||||
let sortOrder: number;
|
||||
let sortOrder: number = 65535;
|
||||
//based on the next and previous labelMap calculate current sort order
|
||||
if (prevSortOrder && nextSortOrder) {
|
||||
sortOrder = (prevSortOrder + nextSortOrder) / 2;
|
||||
} else if (nextSortOrder) {
|
||||
sortOrder = nextSortOrder + 10000;
|
||||
} else {
|
||||
sortOrder = prevSortOrder! / 2;
|
||||
sortOrder = nextSortOrder / 2;
|
||||
} else if (prevSortOrder) {
|
||||
sortOrder = prevSortOrder + 10000;
|
||||
}
|
||||
data.sort_order = sortOrder;
|
||||
}
|
||||
return this.updateLabel(workspaceSlug, projectId, labelId, data);
|
||||
|
||||
return this.updateLabel(workspaceSlug, projectId, draggingLabelId, data);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -37,6 +37,14 @@
|
||||
"@atlaskit/pragmatic-drag-and-drop" "^1.1.0"
|
||||
"@babel/runtime" "^7.0.0"
|
||||
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@atlaskit/pragmatic-drag-and-drop-hitbox/-/pragmatic-drag-and-drop-hitbox-1.0.3.tgz#fcce42f5e2c5a26007f4422397acad29608d3784"
|
||||
integrity sha512-/Sbu/HqN2VGLYBhnsG7SbRNg98XKkbF6L7XDdBi+izRybfaK1FeMfodPpm/xnBHPJzwYMdkE0qtLyv6afhgMUA==
|
||||
dependencies:
|
||||
"@atlaskit/pragmatic-drag-and-drop" "^1.1.0"
|
||||
"@babel/runtime" "^7.0.0"
|
||||
|
||||
"@atlaskit/pragmatic-drag-and-drop@^1.1.0", "@atlaskit/pragmatic-drag-and-drop@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.1.3.tgz#ecbfa4dcd2f9bf9b87f3d1565cedb2661d1fae0a"
|
||||
|
Loading…
Reference in New Issue
Block a user