forked from github/plane
feat: drag and drop an issue to delete (#270)
* feat: drag and drop an issue to delete * style: repositioned trash box
This commit is contained in:
parent
0a88b3ed84
commit
d0afa486c7
@ -1,5 +1,3 @@
|
||||
// react-beautiful-dnd
|
||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||
// hooks
|
||||
import useIssueView from "hooks/use-issue-view";
|
||||
// components
|
||||
@ -15,7 +13,7 @@ type Props = {
|
||||
addIssueToState: (groupTitle: string, stateId: string | null) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
handleOnDragEnd: (result: DropResult) => void;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
@ -27,7 +25,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
addIssueToState,
|
||||
openIssuesListModal,
|
||||
handleDeleteIssue,
|
||||
handleOnDragEnd,
|
||||
handleTrashBox,
|
||||
userAuth,
|
||||
}) => {
|
||||
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssueView(issues);
|
||||
@ -36,7 +34,6 @@ export const AllBoards: React.FC<Props> = ({
|
||||
<>
|
||||
{groupedByIssues ? (
|
||||
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
|
||||
@ -64,6 +61,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
openIssuesListModal={openIssuesListModal ?? null}
|
||||
orderBy={orderBy}
|
||||
handleTrashBox={handleTrashBox}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
);
|
||||
@ -71,7 +69,6 @@ export const AllBoards: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||
|
@ -29,6 +29,7 @@ type Props = {
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
orderBy: NestedKeyOf<IIssue> | "manual" | null;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
@ -43,6 +44,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
handleDeleteIssue,
|
||||
openIssuesListModal,
|
||||
orderBy,
|
||||
handleTrashBox,
|
||||
userAuth,
|
||||
}) => {
|
||||
// collapse/expand
|
||||
@ -89,17 +91,27 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{groupedByIssues[groupTitle].map((issue, index: number) => (
|
||||
<Draggable
|
||||
key={issue.id}
|
||||
draggableId={issue.id}
|
||||
index={index}
|
||||
isDragDisabled={selectedGroup === "created_by"}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<SingleBoardIssue
|
||||
key={index}
|
||||
index={index}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
type={type}
|
||||
issue={issue}
|
||||
selectedGroup={selectedGroup}
|
||||
properties={properties}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
orderBy={orderBy}
|
||||
handleTrashBox={handleTrashBox}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
<span
|
||||
style={{
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import {
|
||||
Draggable,
|
||||
DraggableProvided,
|
||||
DraggableStateSnapshot,
|
||||
DraggingStyle,
|
||||
NotDraggingStyle,
|
||||
@ -37,24 +37,26 @@ import {
|
||||
import { CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
index: number;
|
||||
type?: string;
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
issue: IIssue;
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
properties: Properties;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
orderBy: NestedKeyOf<IIssue> | "manual" | null;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
export const SingleBoardIssue: React.FC<Props> = ({
|
||||
index,
|
||||
type,
|
||||
provided,
|
||||
snapshot,
|
||||
issue,
|
||||
selectedGroup,
|
||||
properties,
|
||||
handleDeleteIssue,
|
||||
orderBy,
|
||||
handleTrashBox,
|
||||
userAuth,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@ -151,14 +153,11 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
useEffect(() => {
|
||||
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
|
||||
}, [snapshot, handleTrashBox]);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={issue.id}
|
||||
draggableId={issue.id}
|
||||
index={index}
|
||||
isDragDisabled={selectedGroup === "created_by"}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`rounded border bg-white shadow-sm ${
|
||||
snapshot.isDragging ? "border-theme bg-indigo-50 shadow-lg" : ""
|
||||
@ -220,8 +219,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
)}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
{issue.sub_issues_count}{" "}
|
||||
{issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
@ -234,7 +232,5 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DropResult } from "react-beautiful-dnd";
|
||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import stateService from "services/state.service";
|
||||
@ -16,6 +16,9 @@ import useIssueView from "hooks/use-issue-view";
|
||||
// components
|
||||
import { AllLists, AllBoards } from "components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
// icons
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
@ -58,6 +61,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [issueToDelete, setIssueToDelete] = useState<IIssue | null>(null);
|
||||
|
||||
// trash box
|
||||
const [trashBox, setTrashBox] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
@ -78,14 +84,27 @@ export const IssuesView: React.FC<Props> = ({
|
||||
: null
|
||||
);
|
||||
|
||||
const handleDeleteIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setDeleteIssueModal(true);
|
||||
setIssueToDelete(issue);
|
||||
},
|
||||
[setDeleteIssueModal, setIssueToDelete]
|
||||
);
|
||||
|
||||
const handleOnDragEnd = useCallback(
|
||||
(result: DropResult) => {
|
||||
setTrashBox(false);
|
||||
|
||||
if (!result.destination || !workspaceSlug || !projectId) return;
|
||||
|
||||
const { source, destination } = result;
|
||||
|
||||
const draggedItem = groupedByIssues[source.droppableId][source.index];
|
||||
|
||||
if (destination.droppableId === "trashBox") {
|
||||
handleDeleteIssue(draggedItem);
|
||||
} else {
|
||||
if (source.droppableId !== destination.droppableId) {
|
||||
const sourceGroup = source.droppableId; // source group id
|
||||
const destinationGroup = destination.droppableId; // destination group id
|
||||
@ -266,11 +285,22 @@ export const IssuesView: React.FC<Props> = ({
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[workspaceSlug, cycleId, moduleId, groupedByIssues, projectId, selectedGroup, states]
|
||||
[
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
groupedByIssues,
|
||||
projectId,
|
||||
selectedGroup,
|
||||
states,
|
||||
handleDeleteIssue,
|
||||
]
|
||||
);
|
||||
|
||||
const addIssueToState = (groupTitle: string, stateId: string | null) => {
|
||||
const addIssueToState = useCallback(
|
||||
(groupTitle: string, stateId: string | null) => {
|
||||
setCreateIssueModal(true);
|
||||
if (selectedGroup)
|
||||
setPreloadedData({
|
||||
@ -279,9 +309,12 @@ export const IssuesView: React.FC<Props> = ({
|
||||
actionType: "createIssue",
|
||||
});
|
||||
else setPreloadedData({ actionType: "createIssue" });
|
||||
};
|
||||
},
|
||||
[setCreateIssueModal, setPreloadedData, selectedGroup]
|
||||
);
|
||||
|
||||
const handleEditIssue = (issue: IIssue) => {
|
||||
const handleEditIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setEditIssueModal(true);
|
||||
setIssueToEdit({
|
||||
...issue,
|
||||
@ -289,14 +322,12 @@ export const IssuesView: React.FC<Props> = ({
|
||||
cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null,
|
||||
module: issue.issue_module ? issue.issue_module.module : null,
|
||||
});
|
||||
};
|
||||
},
|
||||
[setEditIssueModal, setIssueToEdit]
|
||||
);
|
||||
|
||||
const handleDeleteIssue = (issue: IIssue) => {
|
||||
setDeleteIssueModal(true);
|
||||
setIssueToDelete(issue);
|
||||
};
|
||||
|
||||
const removeIssueFromCycle = (bridgeId: string) => {
|
||||
const removeIssueFromCycle = useCallback(
|
||||
(bridgeId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
mutate<CycleIssueResponse[]>(
|
||||
@ -318,9 +349,12 @@ export const IssuesView: React.FC<Props> = ({
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
},
|
||||
[workspaceSlug, projectId, cycleId]
|
||||
);
|
||||
|
||||
const removeIssueFromModule = (bridgeId: string) => {
|
||||
const removeIssueFromModule = useCallback(
|
||||
(bridgeId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
mutate<ModuleIssueResponse[]>(
|
||||
@ -342,7 +376,16 @@ export const IssuesView: React.FC<Props> = ({
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId]
|
||||
);
|
||||
|
||||
const handleTrashBox = useCallback(
|
||||
(isDragging: boolean) => {
|
||||
if (isDragging && !trashBox) setTrashBox(true);
|
||||
},
|
||||
[trashBox, setTrashBox]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -364,6 +407,25 @@ export const IssuesView: React.FC<Props> = ({
|
||||
isOpen={deleteIssueModal}
|
||||
data={issueToDelete}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<StrictModeDroppable droppableId="trashBox">
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`${
|
||||
trashBox ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
||||
} fixed z-20 top-12 left-1/2 -translate-x-1/2 flex items-center gap-2 bg-red-100 border-2 border-red-500 p-3 text-xs rounded ${
|
||||
snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
|
||||
} duration-200`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
Drop issue here to delete
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
{issueView === "list" ? (
|
||||
<AllLists
|
||||
type={type}
|
||||
@ -392,10 +454,12 @@ export const IssuesView: React.FC<Props> = ({
|
||||
addIssueToState={addIssueToState}
|
||||
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
handleOnDragEnd={handleOnDragEnd}
|
||||
handleTrashBox={handleTrashBox}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user