forked from github/plane
1b55411919
* Kanban DnD improvement * minor fixes for kanban dnd improvement * change scroll duration * fix feedback on the UX * add highlight before drop * add toast message explain drag and drop is currently disabled * Change warning dnd message * add comments * fix minor build error
234 lines
8.0 KiB
TypeScript
234 lines
8.0 KiB
TypeScript
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 { observer } from "mobx-react-lite";
|
|
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 { cn } from "@/helpers/common.helper";
|
|
import { useApplication, useIssueDetail, useKanbanView, useProject } from "@/hooks/store";
|
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
|
// components
|
|
import { TRenderQuickActions } from "../list/list-view-types";
|
|
import { IssueProperties } from "../properties/all-properties";
|
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
|
// ui
|
|
// types
|
|
// helper
|
|
|
|
interface IssueBlockProps {
|
|
issueId: string;
|
|
issuesMap: IIssueMap;
|
|
displayProperties: IIssueDisplayProperties | undefined;
|
|
isDragDisabled: boolean;
|
|
draggableId: string;
|
|
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
|
quickActions: TRenderQuickActions;
|
|
canEditProperties: (projectId: string | undefined) => boolean;
|
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
|
issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization
|
|
}
|
|
|
|
interface IssueDetailsBlockProps {
|
|
cardRef: React.RefObject<HTMLElement>;
|
|
issue: TIssue;
|
|
displayProperties: IIssueDisplayProperties | undefined;
|
|
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
|
quickActions: TRenderQuickActions;
|
|
isReadOnly: boolean;
|
|
}
|
|
|
|
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props) => {
|
|
const { cardRef, issue, updateIssue, quickActions, isReadOnly, displayProperties } = props;
|
|
// hooks
|
|
const { isMobile } = usePlatformOS();
|
|
const { getProjectIdentifierById } = useProject();
|
|
|
|
const handleEventPropagation = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<WithDisplayPropertiesHOC displayProperties={displayProperties || {}} displayPropertyKey="key">
|
|
<div className="relative">
|
|
<div className="line-clamp-1 text-xs text-custom-text-300">
|
|
{getProjectIdentifierById(issue.project_id)}-{issue.sequence_id}
|
|
</div>
|
|
<div
|
|
className="absolute -top-1 right-0 hidden group-hover/kanban-block:block"
|
|
onClick={handleEventPropagation}
|
|
>
|
|
{quickActions({
|
|
issue,
|
|
parentRef: cardRef,
|
|
})}
|
|
</div>
|
|
</div>
|
|
</WithDisplayPropertiesHOC>
|
|
|
|
{issue?.is_draft ? (
|
|
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
|
<span>{issue.name}</span>
|
|
</Tooltip>
|
|
) : (
|
|
<div className="w-full line-clamp-1 text-sm text-custom-text-100 mb-1.5">
|
|
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
|
<span>{issue.name}</span>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
|
|
<IssueProperties
|
|
className="flex flex-wrap items-center gap-2 whitespace-nowrap text-custom-text-300 pt-1.5"
|
|
issue={issue}
|
|
displayProperties={displayProperties}
|
|
activeLayout="Kanban"
|
|
updateIssue={updateIssue}
|
|
isReadOnly={isReadOnly}
|
|
/>
|
|
</>
|
|
);
|
|
});
|
|
|
|
export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
|
const {
|
|
issueId,
|
|
issuesMap,
|
|
displayProperties,
|
|
isDragDisabled,
|
|
updateIssue,
|
|
quickActions,
|
|
canEditProperties,
|
|
scrollableContainerRef,
|
|
issueIds,
|
|
} = props;
|
|
|
|
const cardRef = useRef<HTMLAnchorElement | null>(null);
|
|
const {
|
|
router: { workspaceSlug },
|
|
} = useApplication();
|
|
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
|
|
|
const handleIssuePeekOverview = (issue: TIssue) =>
|
|
workspaceSlug &&
|
|
issue &&
|
|
issue.project_id &&
|
|
issue.id &&
|
|
!getIsIssuePeeked(issue.id) &&
|
|
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
|
|
|
const issue = issuesMap[issueId];
|
|
|
|
const { setIsDragging: setIsKanbanDragging } = useKanbanView();
|
|
|
|
const [isDraggingOverBlock, setIsDraggingOverBlock] = useState(false);
|
|
const [isCurrentBlockDragging, setIsCurrentBlockDragging] = useState(false);
|
|
|
|
const canEditIssueProperties = canEditProperties(issue?.project_id);
|
|
|
|
const isDragAllowed = !isDragDisabled && !issue?.tempId && canEditIssueProperties;
|
|
|
|
useOutsideClickDetector(cardRef, () => {
|
|
cardRef?.current?.classList?.remove("highlight");
|
|
});
|
|
|
|
// Make Issue block both as as Draggable and,
|
|
// as a DropTarget for other issues being dragged to get the location of drop
|
|
useEffect(() => {
|
|
const element = cardRef.current;
|
|
|
|
if (!element) return;
|
|
|
|
return combine(
|
|
draggable({
|
|
element,
|
|
dragHandle: element,
|
|
canDrag: () => isDragAllowed,
|
|
getInitialData: () => ({ id: issue?.id, type: "ISSUE" }),
|
|
onDragStart: () => {
|
|
setIsCurrentBlockDragging(true);
|
|
setIsKanbanDragging(true);
|
|
},
|
|
onDrop: () => {
|
|
setIsKanbanDragging(false);
|
|
setIsCurrentBlockDragging(false);
|
|
},
|
|
}),
|
|
dropTargetForElements({
|
|
element,
|
|
getData: () => ({ id: issue?.id, type: "ISSUE" }),
|
|
onDragEnter: () => {
|
|
setIsDraggingOverBlock(true);
|
|
},
|
|
onDragLeave: () => {
|
|
setIsDraggingOverBlock(false);
|
|
},
|
|
onDrop: () => {
|
|
setIsDraggingOverBlock(false);
|
|
},
|
|
})
|
|
);
|
|
}, [cardRef?.current, issue?.id, setIsCurrentBlockDragging, setIsDraggingOverBlock]);
|
|
|
|
if (!issue) return null;
|
|
|
|
return (
|
|
<>
|
|
<DropIndicator isVisible={!isCurrentBlockDragging && isDraggingOverBlock} />
|
|
<div
|
|
// make Z-index higher at the beginning of drag, to have a issue drag image of issue block without any overlaps
|
|
className={cn("group/kanban-block relative p-1.5", { "z-[1]": isCurrentBlockDragging })}
|
|
onDragStart={() => {
|
|
if (isDragAllowed) setIsCurrentBlockDragging(true);
|
|
else
|
|
setToast({
|
|
type: TOAST_TYPE.WARNING,
|
|
title: "Cannot move issue",
|
|
message: "Drag and drop is disabled for the current grouping",
|
|
});
|
|
}}
|
|
>
|
|
<ControlLink
|
|
id={`issue-${issue.id}`}
|
|
href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${
|
|
issue.id
|
|
}`}
|
|
ref={cardRef}
|
|
className={cn(
|
|
"block rounded border-[1px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
|
{ "hover:cursor-pointer": isDragAllowed },
|
|
{ "border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id) },
|
|
{ "bg-custom-background-80 z-[100]": isCurrentBlockDragging }
|
|
)}
|
|
target="_blank"
|
|
onClick={() => handleIssuePeekOverview(issue)}
|
|
disabled={!!issue?.tempId}
|
|
>
|
|
<RenderIfVisible
|
|
classNames="space-y-2 px-3 py-2"
|
|
root={scrollableContainerRef}
|
|
defaultHeight="100px"
|
|
horizontalOffset={50}
|
|
changingReference={issueIds}
|
|
>
|
|
<KanbanIssueDetailsBlock
|
|
cardRef={cardRef}
|
|
issue={issue}
|
|
displayProperties={displayProperties}
|
|
updateIssue={updateIssue}
|
|
quickActions={quickActions}
|
|
isReadOnly={!canEditIssueProperties}
|
|
/>
|
|
</RenderIfVisible>
|
|
</ControlLink>
|
|
</div>
|
|
</>
|
|
);
|
|
});
|
|
|
|
KanbanIssueBlock.displayName = "KanbanIssueBlock";
|