forked from github/plane
d0afa486c7
* feat: drag and drop an issue to delete * style: repositioned trash box
466 lines
15 KiB
TypeScript
466 lines
15 KiB
TypeScript
import { useCallback, useState } from "react";
|
|
|
|
import { useRouter } from "next/router";
|
|
|
|
import useSWR, { mutate } from "swr";
|
|
|
|
// react-beautiful-dnd
|
|
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
|
// services
|
|
import issuesService from "services/issues.service";
|
|
import stateService from "services/state.service";
|
|
import projectService from "services/project.service";
|
|
import modulesService from "services/modules.service";
|
|
// hooks
|
|
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
|
|
import { CycleIssueResponse, IIssue, IssueResponse, ModuleIssueResponse, UserAuth } from "types";
|
|
// fetch-keys
|
|
import {
|
|
CYCLE_ISSUES,
|
|
MODULE_ISSUES,
|
|
PROJECT_ISSUES_LIST,
|
|
PROJECT_MEMBERS,
|
|
STATE_LIST,
|
|
} from "constants/fetch-keys";
|
|
|
|
type Props = {
|
|
type?: "issue" | "cycle" | "module";
|
|
issues: IIssue[];
|
|
openIssuesListModal?: () => void;
|
|
userAuth: UserAuth;
|
|
};
|
|
|
|
export const IssuesView: React.FC<Props> = ({
|
|
type = "issue",
|
|
issues,
|
|
openIssuesListModal,
|
|
userAuth,
|
|
}) => {
|
|
// create issue modal
|
|
const [createIssueModal, setCreateIssueModal] = useState(false);
|
|
const [preloadedData, setPreloadedData] = useState<
|
|
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
|
>(undefined);
|
|
|
|
// updates issue modal
|
|
const [editIssueModal, setEditIssueModal] = useState(false);
|
|
const [issueToEdit, setIssueToEdit] = useState<
|
|
(IIssue & { actionType: "edit" | "delete" }) | undefined
|
|
>(undefined);
|
|
|
|
// delete issue modal
|
|
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;
|
|
|
|
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
|
|
|
|
const { data: stateGroups } = useSWR(
|
|
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
|
workspaceSlug
|
|
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
|
: null
|
|
);
|
|
const states = getStatesList(stateGroups ?? {});
|
|
|
|
const { data: members } = useSWR(
|
|
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
|
workspaceSlug && projectId
|
|
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
|
: 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
|
|
|
|
if (!sourceGroup || !destinationGroup) return;
|
|
|
|
if (selectedGroup === "priority") {
|
|
// update the removed item for mutation
|
|
draggedItem.priority = destinationGroup;
|
|
|
|
if (cycleId)
|
|
mutate<CycleIssueResponse[]>(
|
|
CYCLE_ISSUES(cycleId as string),
|
|
(prevData) => {
|
|
if (!prevData) return prevData;
|
|
const updatedIssues = prevData.map((issue) => {
|
|
if (issue.issue_detail.id === draggedItem.id) {
|
|
return {
|
|
...issue,
|
|
issue_detail: {
|
|
...draggedItem,
|
|
priority: destinationGroup,
|
|
},
|
|
};
|
|
}
|
|
return issue;
|
|
});
|
|
return [...updatedIssues];
|
|
},
|
|
false
|
|
);
|
|
|
|
if (moduleId)
|
|
mutate<ModuleIssueResponse[]>(
|
|
MODULE_ISSUES(moduleId as string),
|
|
(prevData) => {
|
|
if (!prevData) return prevData;
|
|
const updatedIssues = prevData.map((issue) => {
|
|
if (issue.issue_detail.id === draggedItem.id) {
|
|
return {
|
|
...issue,
|
|
issue_detail: {
|
|
...draggedItem,
|
|
priority: destinationGroup,
|
|
},
|
|
};
|
|
}
|
|
return issue;
|
|
});
|
|
return [...updatedIssues];
|
|
},
|
|
false
|
|
);
|
|
|
|
mutate<IssueResponse>(
|
|
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
|
(prevData) => {
|
|
if (!prevData) return prevData;
|
|
|
|
const updatedIssues = prevData.results.map((issue) => {
|
|
if (issue.id === draggedItem.id)
|
|
return {
|
|
...draggedItem,
|
|
priority: destinationGroup,
|
|
};
|
|
|
|
return issue;
|
|
});
|
|
|
|
return {
|
|
...prevData,
|
|
results: updatedIssues,
|
|
};
|
|
},
|
|
false
|
|
);
|
|
|
|
// patch request
|
|
issuesService
|
|
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
|
priority: destinationGroup,
|
|
})
|
|
.then((res) => {
|
|
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
|
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
|
|
|
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
|
});
|
|
} else if (selectedGroup === "state_detail.name") {
|
|
const destinationState = states?.find((s) => s.name === destinationGroup);
|
|
const destinationStateId = destinationState?.id;
|
|
|
|
// update the removed item for mutation
|
|
if (!destinationStateId || !destinationState) return;
|
|
draggedItem.state = destinationStateId;
|
|
draggedItem.state_detail = destinationState;
|
|
|
|
if (cycleId)
|
|
mutate<CycleIssueResponse[]>(
|
|
CYCLE_ISSUES(cycleId as string),
|
|
(prevData) => {
|
|
if (!prevData) return prevData;
|
|
const updatedIssues = prevData.map((issue) => {
|
|
if (issue.issue_detail.id === draggedItem.id) {
|
|
return {
|
|
...issue,
|
|
issue_detail: {
|
|
...draggedItem,
|
|
state_detail: destinationState,
|
|
state: destinationStateId,
|
|
},
|
|
};
|
|
}
|
|
return issue;
|
|
});
|
|
return [...updatedIssues];
|
|
},
|
|
false
|
|
);
|
|
|
|
if (moduleId)
|
|
mutate<ModuleIssueResponse[]>(
|
|
MODULE_ISSUES(moduleId as string),
|
|
(prevData) => {
|
|
if (!prevData) return prevData;
|
|
const updatedIssues = prevData.map((issue) => {
|
|
if (issue.issue_detail.id === draggedItem.id) {
|
|
return {
|
|
...issue,
|
|
issue_detail: {
|
|
...draggedItem,
|
|
state_detail: destinationState,
|
|
state: destinationStateId,
|
|
},
|
|
};
|
|
}
|
|
return issue;
|
|
});
|
|
return [...updatedIssues];
|
|
},
|
|
false
|
|
);
|
|
|
|
mutate<IssueResponse>(
|
|
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
|
(prevData) => {
|
|
if (!prevData) return prevData;
|
|
|
|
const updatedIssues = prevData.results.map((issue) => {
|
|
if (issue.id === draggedItem.id)
|
|
return {
|
|
...draggedItem,
|
|
state_detail: destinationState,
|
|
state: destinationStateId,
|
|
};
|
|
|
|
return issue;
|
|
});
|
|
|
|
return {
|
|
...prevData,
|
|
results: updatedIssues,
|
|
};
|
|
},
|
|
false
|
|
);
|
|
|
|
// patch request
|
|
issuesService
|
|
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
|
state: destinationStateId,
|
|
})
|
|
.then((res) => {
|
|
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
|
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
|
|
|
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[
|
|
workspaceSlug,
|
|
cycleId,
|
|
moduleId,
|
|
groupedByIssues,
|
|
projectId,
|
|
selectedGroup,
|
|
states,
|
|
handleDeleteIssue,
|
|
]
|
|
);
|
|
|
|
const addIssueToState = useCallback(
|
|
(groupTitle: string, stateId: string | null) => {
|
|
setCreateIssueModal(true);
|
|
if (selectedGroup)
|
|
setPreloadedData({
|
|
state: stateId ?? undefined,
|
|
[selectedGroup]: groupTitle,
|
|
actionType: "createIssue",
|
|
});
|
|
else setPreloadedData({ actionType: "createIssue" });
|
|
},
|
|
[setCreateIssueModal, setPreloadedData, selectedGroup]
|
|
);
|
|
|
|
const handleEditIssue = useCallback(
|
|
(issue: IIssue) => {
|
|
setEditIssueModal(true);
|
|
setIssueToEdit({
|
|
...issue,
|
|
actionType: "edit",
|
|
cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null,
|
|
module: issue.issue_module ? issue.issue_module.module : null,
|
|
});
|
|
},
|
|
[setEditIssueModal, setIssueToEdit]
|
|
);
|
|
|
|
const removeIssueFromCycle = useCallback(
|
|
(bridgeId: string) => {
|
|
if (!workspaceSlug || !projectId) return;
|
|
|
|
mutate<CycleIssueResponse[]>(
|
|
CYCLE_ISSUES(cycleId as string),
|
|
(prevData) => prevData?.filter((p) => p.id !== bridgeId),
|
|
false
|
|
);
|
|
|
|
issuesService
|
|
.removeIssueFromCycle(
|
|
workspaceSlug as string,
|
|
projectId as string,
|
|
cycleId as string,
|
|
bridgeId
|
|
)
|
|
.then((res) => {
|
|
console.log(res);
|
|
})
|
|
.catch((e) => {
|
|
console.log(e);
|
|
});
|
|
},
|
|
[workspaceSlug, projectId, cycleId]
|
|
);
|
|
|
|
const removeIssueFromModule = useCallback(
|
|
(bridgeId: string) => {
|
|
if (!workspaceSlug || !projectId) return;
|
|
|
|
mutate<ModuleIssueResponse[]>(
|
|
MODULE_ISSUES(moduleId as string),
|
|
(prevData) => prevData?.filter((p) => p.id !== bridgeId),
|
|
false
|
|
);
|
|
|
|
modulesService
|
|
.removeIssueFromModule(
|
|
workspaceSlug as string,
|
|
projectId as string,
|
|
moduleId as string,
|
|
bridgeId
|
|
)
|
|
.then((res) => {
|
|
console.log(res);
|
|
})
|
|
.catch((e) => {
|
|
console.log(e);
|
|
});
|
|
},
|
|
[workspaceSlug, projectId, moduleId]
|
|
);
|
|
|
|
const handleTrashBox = useCallback(
|
|
(isDragging: boolean) => {
|
|
if (isDragging && !trashBox) setTrashBox(true);
|
|
},
|
|
[trashBox, setTrashBox]
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<CreateUpdateIssueModal
|
|
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
|
handleClose={() => setCreateIssueModal(false)}
|
|
prePopulateData={{
|
|
...preloadedData,
|
|
}}
|
|
/>
|
|
<CreateUpdateIssueModal
|
|
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
|
prePopulateData={{ ...issueToEdit }}
|
|
handleClose={() => setEditIssueModal(false)}
|
|
data={issueToEdit}
|
|
/>
|
|
<DeleteIssueModal
|
|
handleClose={() => setDeleteIssueModal(false)}
|
|
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}
|
|
issues={issues}
|
|
states={states}
|
|
members={members}
|
|
addIssueToState={addIssueToState}
|
|
handleEditIssue={handleEditIssue}
|
|
handleDeleteIssue={handleDeleteIssue}
|
|
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
|
removeIssue={
|
|
type === "cycle"
|
|
? removeIssueFromCycle
|
|
: type === "module"
|
|
? removeIssueFromModule
|
|
: null
|
|
}
|
|
userAuth={userAuth}
|
|
/>
|
|
) : (
|
|
<AllBoards
|
|
type={type}
|
|
issues={issues}
|
|
states={states}
|
|
members={members}
|
|
addIssueToState={addIssueToState}
|
|
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
|
handleDeleteIssue={handleDeleteIssue}
|
|
handleTrashBox={handleTrashBox}
|
|
userAuth={userAuth}
|
|
/>
|
|
)}
|
|
</DragDropContext>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|