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:
Aaryan Khandelwal 2023-02-13 10:32:02 +05:30 committed by GitHub
parent 0a88b3ed84
commit d0afa486c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 447 additions and 378 deletions

View File

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

View File

@ -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={{

View File

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

View File

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