forked from github/plane
240 lines
8.6 KiB
TypeScript
240 lines
8.6 KiB
TypeScript
import React, { useCallback, useEffect, useState } from "react";
|
|
// next
|
|
import { useRouter } from "next/router";
|
|
// swr
|
|
import useSWR from "swr";
|
|
// react beautiful dnd
|
|
import type { DropResult } from "react-beautiful-dnd";
|
|
import { DragDropContext } from "react-beautiful-dnd";
|
|
// services
|
|
import stateServices from "lib/services/state.service";
|
|
import issuesServices from "lib/services/issues.service";
|
|
// hooks
|
|
import useUser from "lib/hooks/useUser";
|
|
// fetching keys
|
|
import { STATE_LIST } from "constants/fetch-keys";
|
|
// components
|
|
import SingleBoard from "components/project/issues/BoardView/SingleBoard";
|
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
|
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
|
// ui
|
|
import { Spinner } from "ui";
|
|
// types
|
|
import type { IState, IIssue, Properties, NestedKeyOf, IProjectMember } from "types";
|
|
import ConfirmIssueDeletion from "../ConfirmIssueDeletion";
|
|
import { TrashIcon } from "@heroicons/react/24/outline";
|
|
|
|
type Props = {
|
|
properties: Properties;
|
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
|
groupedByIssues: {
|
|
[key: string]: IIssue[];
|
|
};
|
|
members: IProjectMember[] | undefined;
|
|
};
|
|
|
|
const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues, members }) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const [isIssueOpen, setIsIssueOpen] = useState(false);
|
|
const [isIssueDeletionOpen, setIsIssueDeletionOpen] = useState(false);
|
|
const [issueDeletionData, setIssueDeletionData] = useState<IIssue | undefined>();
|
|
|
|
const [preloadedData, setPreloadedData] = useState<
|
|
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
|
>(undefined);
|
|
|
|
const { activeWorkspace } = useUser();
|
|
|
|
const router = useRouter();
|
|
|
|
const { projectId } = router.query;
|
|
|
|
const { data: states, mutate: mutateState } = useSWR<IState[]>(
|
|
projectId && activeWorkspace ? STATE_LIST(projectId as string) : null,
|
|
activeWorkspace
|
|
? () => stateServices.getStates(activeWorkspace.slug, projectId as string)
|
|
: null
|
|
);
|
|
|
|
const handleOnDragEnd = useCallback(
|
|
(result: DropResult) => {
|
|
if (!result.destination) return;
|
|
const { source, destination, type } = result;
|
|
|
|
if (destination.droppableId === "trashBox") {
|
|
const removedItem = groupedByIssues[source.droppableId][source.index];
|
|
|
|
setIssueDeletionData(removedItem);
|
|
setIsIssueDeletionOpen(true);
|
|
} else {
|
|
if (type === "state") {
|
|
const newStates = Array.from(states ?? []);
|
|
const [reorderedState] = newStates.splice(source.index, 1);
|
|
newStates.splice(destination.index, 0, reorderedState);
|
|
const prevSequenceNumber = newStates[destination.index - 1]?.sequence;
|
|
const nextSequenceNumber = newStates[destination.index + 1]?.sequence;
|
|
|
|
const sequenceNumber =
|
|
prevSequenceNumber && nextSequenceNumber
|
|
? (prevSequenceNumber + nextSequenceNumber) / 2
|
|
: nextSequenceNumber
|
|
? nextSequenceNumber - 15000 / 2
|
|
: prevSequenceNumber
|
|
? prevSequenceNumber + 15000 / 2
|
|
: 15000;
|
|
|
|
newStates[destination.index].sequence = sequenceNumber;
|
|
|
|
mutateState(newStates, false);
|
|
if (!activeWorkspace) return;
|
|
stateServices
|
|
.patchState(
|
|
activeWorkspace.slug,
|
|
projectId as string,
|
|
newStates[destination.index].id,
|
|
{
|
|
sequence: sequenceNumber,
|
|
}
|
|
)
|
|
.then((response) => {
|
|
console.log(response);
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
});
|
|
} else {
|
|
if (source.droppableId !== destination.droppableId) {
|
|
const sourceGroup = source.droppableId; // source group id
|
|
const destinationGroup = destination.droppableId; // destination group id
|
|
if (!sourceGroup || !destinationGroup) return;
|
|
|
|
// removed/dragged item
|
|
const removedItem = groupedByIssues[source.droppableId][source.index];
|
|
|
|
if (selectedGroup === "priority") {
|
|
// update the removed item for mutation
|
|
removedItem.priority = destinationGroup;
|
|
|
|
// patch request
|
|
issuesServices.patchIssue(
|
|
activeWorkspace!.slug,
|
|
projectId as string,
|
|
removedItem.id,
|
|
{
|
|
priority: destinationGroup,
|
|
}
|
|
);
|
|
} 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;
|
|
removedItem.state = destinationStateId;
|
|
removedItem.state_detail = destinationState;
|
|
|
|
// patch request
|
|
issuesServices.patchIssue(
|
|
activeWorkspace!.slug,
|
|
projectId as string,
|
|
removedItem.id,
|
|
{
|
|
state: destinationStateId,
|
|
}
|
|
);
|
|
}
|
|
|
|
// remove item from the source group
|
|
groupedByIssues[source.droppableId].splice(source.index, 1);
|
|
// add item to the destination group
|
|
groupedByIssues[destination.droppableId].splice(destination.index, 0, removedItem);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[activeWorkspace, mutateState, groupedByIssues, projectId, selectedGroup, states]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) return;
|
|
const timer = setTimeout(() => {
|
|
setPreloadedData(undefined);
|
|
clearTimeout(timer);
|
|
}, 500);
|
|
}, [isOpen]);
|
|
|
|
return (
|
|
<>
|
|
<ConfirmIssueDeletion
|
|
isOpen={isIssueDeletionOpen}
|
|
handleClose={() => setIsIssueDeletionOpen(false)}
|
|
data={issueDeletionData}
|
|
/>
|
|
<CreateUpdateIssuesModal
|
|
isOpen={isIssueOpen && preloadedData?.actionType === "createIssue"}
|
|
setIsOpen={setIsIssueOpen}
|
|
prePopulateData={{
|
|
...preloadedData,
|
|
}}
|
|
projectId={projectId as string}
|
|
/>
|
|
{groupedByIssues ? (
|
|
<div className="h-full w-full">
|
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
|
<div className="h-full w-full overflow-hidden">
|
|
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
|
|
{(provided) => (
|
|
<div
|
|
className="h-full w-full"
|
|
{...provided.droppableProps}
|
|
ref={provided.innerRef}
|
|
>
|
|
<div className="flex gap-x-4 h-full overflow-x-auto overflow-y-hidden pb-3">
|
|
{Object.keys(groupedByIssues).map((singleGroup, index) => (
|
|
<SingleBoard
|
|
key={singleGroup}
|
|
selectedGroup={selectedGroup}
|
|
groupTitle={singleGroup}
|
|
createdBy={
|
|
selectedGroup === "created_by"
|
|
? members?.find((m) => m.member.id === singleGroup)?.member
|
|
.first_name ?? "loading..."
|
|
: null
|
|
}
|
|
groupedByIssues={groupedByIssues}
|
|
index={index}
|
|
setIsIssueOpen={setIsIssueOpen}
|
|
properties={properties}
|
|
setPreloadedData={setPreloadedData}
|
|
stateId={
|
|
selectedGroup === "state_detail.name"
|
|
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
|
: null
|
|
}
|
|
bgColor={
|
|
selectedGroup === "state_detail.name"
|
|
? states?.find((s) => s.name === singleGroup)?.color
|
|
: undefined
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
{provided.placeholder}
|
|
</div>
|
|
)}
|
|
</StrictModeDroppable>
|
|
</div>
|
|
</DragDropContext>
|
|
</div>
|
|
) : (
|
|
<div className="h-full w-full flex justify-center items-center">
|
|
<Spinner />
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default BoardView;
|