228 lines
7.1 KiB
TypeScript
228 lines
7.1 KiB
TypeScript
import { type FC, useEffect, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "react-toastify";
|
|
import DefaulModal from "../../../../components/DefaulModal";
|
|
import PageLoading from "../../../../components/PageLoading";
|
|
import { ErrorType } from "../../../../helpers/types";
|
|
import { useChangeTaskPhase, useDeleteTask, useGetTaskDetail, useUpdateTask } from "../../task/hooks/useTaskData";
|
|
import { useGetTaskTimes } from "../../task/hooks/useTaskTimeData";
|
|
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
|
import type { Column, Task } from "../../types";
|
|
import AttachmentList from "./attachment/List";
|
|
import CheckLists from "./checklist/List";
|
|
import TimeList from "./time/List";
|
|
import TaskDetailCoverColorBar from "./cover/TaskDetailCoverColorBar";
|
|
import TaskDetailDescription from "./TaskDetailDescription";
|
|
import TaskDetailHeader from "./TaskDetailHeader";
|
|
import TaskDetailToolbar from "./TaskDetailToolbar";
|
|
import type { TaskDetailTab } from "./types";
|
|
|
|
type Props = {
|
|
open: boolean;
|
|
task: Task | null;
|
|
columns: Column[];
|
|
projectId: string;
|
|
onClose: () => void;
|
|
onTaskPhaseChanged?: (taskId: string, taskPhaseId: string) => void;
|
|
};
|
|
|
|
const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, onTaskPhaseChanged }) => {
|
|
const { t } = useTranslation("global");
|
|
const [activeTab, setActiveTab] = useState<TaskDetailTab | null>(null);
|
|
const [title, setTitle] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [color, setColor] = useState<string | null>(null);
|
|
const initialTitleRef = useRef("");
|
|
const initialDescriptionRef = useRef("");
|
|
|
|
const getTaskDetail = useGetTaskDetail(task?.id ?? "", open && !!task?.id);
|
|
const getTaskTimes = useGetTaskTimes(task?.id ?? "", open && !!task?.id);
|
|
const updateTask = useUpdateTask();
|
|
const changeTaskPhase = useChangeTaskPhase();
|
|
const deleteTask = useDeleteTask();
|
|
|
|
const rawTaskDetail = getTaskDetail.data?.data;
|
|
const taskDetail: TaskDetailType | undefined = rawTaskDetail
|
|
? {
|
|
...rawTaskDetail,
|
|
id: rawTaskDetail.id ?? task?.id ?? "",
|
|
labels: rawTaskDetail.labels ?? rawTaskDetail.remarks,
|
|
}
|
|
: undefined;
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setActiveTab(null);
|
|
setTitle("");
|
|
setDescription("");
|
|
setColor(null);
|
|
initialTitleRef.current = "";
|
|
initialDescriptionRef.current = "";
|
|
}
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
if (!rawTaskDetail) return;
|
|
|
|
setTitle(rawTaskDetail.title);
|
|
setDescription(rawTaskDetail.description ?? "");
|
|
setColor(rawTaskDetail.color || null);
|
|
initialTitleRef.current = rawTaskDetail.title;
|
|
initialDescriptionRef.current = rawTaskDetail.description ?? "";
|
|
}, [rawTaskDetail]);
|
|
|
|
if (!task) return null;
|
|
|
|
const selectedPhaseId = task.columnId || taskDetail?.taskPhaseId || "";
|
|
|
|
const handleTabChange = (tab: TaskDetailTab) => {
|
|
setActiveTab((prev) => (prev === tab ? null : tab));
|
|
};
|
|
|
|
const handleDelete = () => {
|
|
if (!task.id) return;
|
|
|
|
deleteTask.mutate(
|
|
{ id: task.id, projectId: taskDetail?.projectId ?? "" },
|
|
{
|
|
onSuccess: () => {
|
|
toast.success(t("taskmanager.task_deleted"));
|
|
onClose();
|
|
},
|
|
onError: (error: ErrorType) => {
|
|
toast.error(error.response?.data?.error.message[0]);
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
const saveField = (field: "title" | "description", value: string) => {
|
|
const initialValue = field === "title" ? initialTitleRef.current : initialDescriptionRef.current;
|
|
if (value === initialValue) return;
|
|
|
|
updateTask.mutate(
|
|
{ id: task.id, params: { [field]: value } },
|
|
{
|
|
onSuccess: () => {
|
|
if (field === "title") {
|
|
initialTitleRef.current = value;
|
|
} else {
|
|
initialDescriptionRef.current = value;
|
|
}
|
|
toast.success(t("success"));
|
|
},
|
|
onError: (error: ErrorType) => {
|
|
toast.error(error.response?.data?.error.message[0]);
|
|
if (field === "title") {
|
|
setTitle(initialTitleRef.current);
|
|
} else {
|
|
setDescription(initialDescriptionRef.current);
|
|
}
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
const handleColorChange = (newColor: string | null) => {
|
|
const previousColor = color;
|
|
setColor(newColor);
|
|
|
|
updateTask.mutate(
|
|
{ id: task.id, params: { color: newColor ?? "" } },
|
|
{
|
|
onSuccess: () => {
|
|
toast.success(t("success"));
|
|
},
|
|
onError: (error: ErrorType) => {
|
|
setColor(previousColor);
|
|
toast.error(error.response?.data?.error.message[0]);
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
const handlePhaseChange = (taskPhaseId: string) => {
|
|
if (taskPhaseId === selectedPhaseId) return;
|
|
|
|
changeTaskPhase.mutate(
|
|
{ id: task.id, params: { taskPhaseId }, projectId: taskDetail?.projectId ?? projectId },
|
|
{
|
|
onSuccess: () => {
|
|
onTaskPhaseChanged?.(task.id, taskPhaseId);
|
|
toast.success(t("success"));
|
|
},
|
|
onError: (error: ErrorType) => {
|
|
toast.error(error.response?.data?.error.message[0]);
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<DefaulModal open={open} close={onClose} width={680}>
|
|
{getTaskDetail.isPending ? (
|
|
<PageLoading />
|
|
) : (
|
|
<>
|
|
<TaskDetailHeader
|
|
columns={columns}
|
|
selectedPhaseId={selectedPhaseId}
|
|
onPhaseChange={handlePhaseChange}
|
|
isChangingPhase={changeTaskPhase.isPending}
|
|
taskColor={color}
|
|
onColorSelect={(newColor) => handleColorChange(newColor)}
|
|
isUpdatingColor={updateTask.isPending}
|
|
onClose={onClose}
|
|
onDelete={handleDelete}
|
|
isDeleting={deleteTask.isPending}
|
|
/>
|
|
|
|
{color ? (
|
|
<TaskDetailCoverColorBar
|
|
color={color}
|
|
onRemove={() => handleColorChange(null)}
|
|
isRemoving={updateTask.isPending}
|
|
/>
|
|
) : null}
|
|
|
|
<input
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
onBlur={() => saveField("title", title)}
|
|
className="mt-6 w-full text-sm font-bold bg-transparent outline-none"
|
|
/>
|
|
|
|
<div className="mt-4">
|
|
<TaskDetailToolbar
|
|
activeTab={activeTab}
|
|
onTabChange={handleTabChange}
|
|
taskDetail={taskDetail}
|
|
taskId={task.id}
|
|
/>
|
|
</div>
|
|
|
|
<TaskDetailDescription
|
|
value={description}
|
|
onChange={setDescription}
|
|
onSave={() => saveField("description", description)}
|
|
isSaving={updateTask.isPending}
|
|
/>
|
|
|
|
<AttachmentList attachments={taskDetail?.attachments} />
|
|
|
|
<CheckLists
|
|
taskId={task.id}
|
|
checkListItems={taskDetail?.checkListItems}
|
|
checkListItemCount={taskDetail?.checkListItemCount}
|
|
completedCheckListItemCount={taskDetail?.completedCheckListItemCount}
|
|
/>
|
|
|
|
<TimeList times={getTaskTimes.data?.data} />
|
|
</>
|
|
)}
|
|
</DefaulModal>
|
|
);
|
|
};
|
|
|
|
export default TaskDetailModal;
|