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 // hooks
import useIssueView from "hooks/use-issue-view"; import useIssueView from "hooks/use-issue-view";
// components // components
@ -15,7 +13,7 @@ type Props = {
addIssueToState: (groupTitle: string, stateId: string | null) => void; addIssueToState: (groupTitle: string, stateId: string | null) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
handleOnDragEnd: (result: DropResult) => void; handleTrashBox: (isDragging: boolean) => void;
userAuth: UserAuth; userAuth: UserAuth;
}; };
@ -27,7 +25,7 @@ export const AllBoards: React.FC<Props> = ({
addIssueToState, addIssueToState,
openIssuesListModal, openIssuesListModal,
handleDeleteIssue, handleDeleteIssue,
handleOnDragEnd, handleTrashBox,
userAuth, userAuth,
}) => { }) => {
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssueView(issues); const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssueView(issues);
@ -36,7 +34,6 @@ export const AllBoards: React.FC<Props> = ({
<> <>
{groupedByIssues ? ( {groupedByIssues ? (
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full"> <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 overflow-hidden">
<div className="h-full w-full"> <div className="h-full w-full">
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden"> <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} handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={openIssuesListModal ?? null} openIssuesListModal={openIssuesListModal ?? null}
orderBy={orderBy} orderBy={orderBy}
handleTrashBox={handleTrashBox}
userAuth={userAuth} userAuth={userAuth}
/> />
); );
@ -71,7 +69,6 @@ export const AllBoards: React.FC<Props> = ({
</div> </div>
</div> </div>
</div> </div>
</DragDropContext>
</div> </div>
) : ( ) : (
<div className="flex h-full w-full items-center justify-center">Loading...</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; handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
orderBy: NestedKeyOf<IIssue> | "manual" | null; orderBy: NestedKeyOf<IIssue> | "manual" | null;
handleTrashBox: (isDragging: boolean) => void;
userAuth: UserAuth; userAuth: UserAuth;
}; };
@ -43,6 +44,7 @@ export const SingleBoard: React.FC<Props> = ({
handleDeleteIssue, handleDeleteIssue,
openIssuesListModal, openIssuesListModal,
orderBy, orderBy,
handleTrashBox,
userAuth, userAuth,
}) => { }) => {
// collapse/expand // collapse/expand
@ -89,17 +91,27 @@ export const SingleBoard: React.FC<Props> = ({
{...provided.droppableProps} {...provided.droppableProps}
> >
{groupedByIssues[groupTitle].map((issue, index: number) => ( {groupedByIssues[groupTitle].map((issue, index: number) => (
<Draggable
key={issue.id}
draggableId={issue.id}
index={index}
isDragDisabled={selectedGroup === "created_by"}
>
{(provided, snapshot) => (
<SingleBoardIssue <SingleBoardIssue
key={index} key={index}
index={index} provided={provided}
snapshot={snapshot}
type={type} type={type}
issue={issue} issue={issue}
selectedGroup={selectedGroup}
properties={properties} properties={properties}
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={handleDeleteIssue}
orderBy={orderBy} orderBy={orderBy}
handleTrashBox={handleTrashBox}
userAuth={userAuth} userAuth={userAuth}
/> />
)}
</Draggable>
))} ))}
<span <span
style={{ style={{

View File

@ -1,13 +1,13 @@
import React, { useCallback } from "react"; import React, { useCallback, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import { mutate } from "swr";
// react-beautiful-dnd // react-beautiful-dnd
import { import {
Draggable, DraggableProvided,
DraggableStateSnapshot, DraggableStateSnapshot,
DraggingStyle, DraggingStyle,
NotDraggingStyle, NotDraggingStyle,
@ -37,24 +37,26 @@ import {
import { CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys"; import { CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
type Props = { type Props = {
index: number;
type?: string; type?: string;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
issue: IIssue; issue: IIssue;
selectedGroup: NestedKeyOf<IIssue> | null;
properties: Properties; properties: Properties;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
orderBy: NestedKeyOf<IIssue> | "manual" | null; orderBy: NestedKeyOf<IIssue> | "manual" | null;
handleTrashBox: (isDragging: boolean) => void;
userAuth: UserAuth; userAuth: UserAuth;
}; };
export const SingleBoardIssue: React.FC<Props> = ({ export const SingleBoardIssue: React.FC<Props> = ({
index,
type, type,
provided,
snapshot,
issue, issue,
selectedGroup,
properties, properties,
handleDeleteIssue, handleDeleteIssue,
orderBy, orderBy,
handleTrashBox,
userAuth, userAuth,
}) => { }) => {
const router = useRouter(); const router = useRouter();
@ -151,14 +153,11 @@ export const SingleBoardIssue: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
useEffect(() => {
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
}, [snapshot, handleTrashBox]);
return ( return (
<Draggable
key={issue.id}
draggableId={issue.id}
index={index}
isDragDisabled={selectedGroup === "created_by"}
>
{(provided, snapshot) => (
<div <div
className={`rounded border bg-white shadow-sm ${ className={`rounded border bg-white shadow-sm ${
snapshot.isDragging ? "border-theme bg-indigo-50 shadow-lg" : "" snapshot.isDragging ? "border-theme bg-indigo-50 shadow-lg" : ""
@ -220,8 +219,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
)} )}
{properties.sub_issue_count && ( {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"> <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} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
{issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div> </div>
)} )}
{properties.assignee && ( {properties.assignee && (
@ -234,7 +232,5 @@ export const SingleBoardIssue: React.FC<Props> = ({
</div> </div>
</div> </div>
</div> </div>
)}
</Draggable>
); );
}; };

View File

@ -5,7 +5,7 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// react-beautiful-dnd // react-beautiful-dnd
import { DropResult } from "react-beautiful-dnd"; import { DragDropContext, DropResult } from "react-beautiful-dnd";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import stateService from "services/state.service"; import stateService from "services/state.service";
@ -16,6 +16,9 @@ import useIssueView from "hooks/use-issue-view";
// components // components
import { AllLists, AllBoards } from "components/core"; import { AllLists, AllBoards } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// icons
import { TrashIcon } from "@heroicons/react/24/outline";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
// types // types
@ -58,6 +61,9 @@ export const IssuesView: React.FC<Props> = ({
const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [issueToDelete, setIssueToDelete] = useState<IIssue | null>(null); const [issueToDelete, setIssueToDelete] = useState<IIssue | null>(null);
// trash box
const [trashBox, setTrashBox] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@ -78,14 +84,27 @@ export const IssuesView: React.FC<Props> = ({
: null : null
); );
const handleDeleteIssue = useCallback(
(issue: IIssue) => {
setDeleteIssueModal(true);
setIssueToDelete(issue);
},
[setDeleteIssueModal, setIssueToDelete]
);
const handleOnDragEnd = useCallback( const handleOnDragEnd = useCallback(
(result: DropResult) => { (result: DropResult) => {
setTrashBox(false);
if (!result.destination || !workspaceSlug || !projectId) return; if (!result.destination || !workspaceSlug || !projectId) return;
const { source, destination } = result; const { source, destination } = result;
const draggedItem = groupedByIssues[source.droppableId][source.index]; const draggedItem = groupedByIssues[source.droppableId][source.index];
if (destination.droppableId === "trashBox") {
handleDeleteIssue(draggedItem);
} else {
if (source.droppableId !== destination.droppableId) { if (source.droppableId !== destination.droppableId) {
const sourceGroup = source.droppableId; // source group id const sourceGroup = source.droppableId; // source group id
const destinationGroup = destination.droppableId; // destination 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); setCreateIssueModal(true);
if (selectedGroup) if (selectedGroup)
setPreloadedData({ setPreloadedData({
@ -279,9 +309,12 @@ export const IssuesView: React.FC<Props> = ({
actionType: "createIssue", actionType: "createIssue",
}); });
else setPreloadedData({ actionType: "createIssue" }); else setPreloadedData({ actionType: "createIssue" });
}; },
[setCreateIssueModal, setPreloadedData, selectedGroup]
);
const handleEditIssue = (issue: IIssue) => { const handleEditIssue = useCallback(
(issue: IIssue) => {
setEditIssueModal(true); setEditIssueModal(true);
setIssueToEdit({ setIssueToEdit({
...issue, ...issue,
@ -289,14 +322,12 @@ export const IssuesView: React.FC<Props> = ({
cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null,
module: issue.issue_module ? issue.issue_module.module : null, module: issue.issue_module ? issue.issue_module.module : null,
}); });
}; },
[setEditIssueModal, setIssueToEdit]
);
const handleDeleteIssue = (issue: IIssue) => { const removeIssueFromCycle = useCallback(
setDeleteIssueModal(true); (bridgeId: string) => {
setIssueToDelete(issue);
};
const removeIssueFromCycle = (bridgeId: string) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
mutate<CycleIssueResponse[]>( mutate<CycleIssueResponse[]>(
@ -318,9 +349,12 @@ export const IssuesView: React.FC<Props> = ({
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
}); });
}; },
[workspaceSlug, projectId, cycleId]
);
const removeIssueFromModule = (bridgeId: string) => { const removeIssueFromModule = useCallback(
(bridgeId: string) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
mutate<ModuleIssueResponse[]>( mutate<ModuleIssueResponse[]>(
@ -342,7 +376,16 @@ export const IssuesView: React.FC<Props> = ({
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
}); });
}; },
[workspaceSlug, projectId, moduleId]
);
const handleTrashBox = useCallback(
(isDragging: boolean) => {
if (isDragging && !trashBox) setTrashBox(true);
},
[trashBox, setTrashBox]
);
return ( return (
<> <>
@ -364,6 +407,25 @@ export const IssuesView: React.FC<Props> = ({
isOpen={deleteIssueModal} isOpen={deleteIssueModal}
data={issueToDelete} 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" ? ( {issueView === "list" ? (
<AllLists <AllLists
type={type} type={type}
@ -392,10 +454,12 @@ export const IssuesView: React.FC<Props> = ({
addIssueToState={addIssueToState} addIssueToState={addIssueToState}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null} openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={handleDeleteIssue}
handleOnDragEnd={handleOnDragEnd} handleTrashBox={handleTrashBox}
userAuth={userAuth} userAuth={userAuth}
/> />
)} )}
</DragDropContext>
</div>
</> </>
); );
}; };