This commit is contained in:
@@ -1,21 +1,61 @@
|
|||||||
|
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
import { ArrowDown2, CloseCircle } from "iconsax-react";
|
import { ArrowDown2, CloseCircle } from "iconsax-react";
|
||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import TrashWithConfrim from "../../../../components/TrashWithConfrim";
|
import TrashWithConfrim from "../../../../components/TrashWithConfrim";
|
||||||
|
import { clx } from "../../../../helpers/utils";
|
||||||
|
import type { Column } from "../../types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
statusLabel: string;
|
columns: Column[];
|
||||||
|
selectedPhaseId: string;
|
||||||
|
onPhaseChange: (phaseId: string) => void;
|
||||||
|
isChangingPhase?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
isDeleting?: boolean;
|
isDeleting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailHeader: FC<Props> = ({ statusLabel, onClose, onDelete, isDeleting }) => {
|
const TaskDetailHeader: FC<Props> = ({
|
||||||
|
columns,
|
||||||
|
selectedPhaseId,
|
||||||
|
onPhaseChange,
|
||||||
|
isChangingPhase,
|
||||||
|
onClose,
|
||||||
|
onDelete,
|
||||||
|
isDeleting,
|
||||||
|
}) => {
|
||||||
|
const selectedColumn = columns.find((column) => column.id === selectedPhaseId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||||
<button type="button" className="flex items-center gap-1.5 bg-white/60 rounded-full px-4 py-2 text-xs font-medium cursor-pointer">
|
<Popover className="relative">
|
||||||
<span>{statusLabel}</span>
|
<PopoverButton
|
||||||
<ArrowDown2 size={14} color="#292D32" />
|
disabled={isChangingPhase}
|
||||||
</button>
|
className="flex items-center gap-1.5 bg-white/60 rounded-full px-4 py-2 text-xs font-medium cursor-pointer disabled:opacity-50 outline-none"
|
||||||
|
>
|
||||||
|
<span>{selectedColumn?.title ?? ""}</span>
|
||||||
|
<ArrowDown2 size={14} color="#292D32" />
|
||||||
|
</PopoverButton>
|
||||||
|
|
||||||
|
<PopoverPanel
|
||||||
|
anchor="bottom start"
|
||||||
|
className="z-20 mt-1 min-w-[160px] rounded-xl bg-white shadow-md border border-border py-1 text-sm"
|
||||||
|
>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<button
|
||||||
|
key={column.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onPhaseChange(column.id)}
|
||||||
|
className={clx(
|
||||||
|
"w-full px-4 py-2.5 text-right transition-colors hover:bg-black/5",
|
||||||
|
column.id === selectedPhaseId && "font-bold bg-black/5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{column.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</PopoverPanel>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{onDelete ? (
|
{onDelete ? (
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { toast } from "react-toastify";
|
|||||||
import DefaulModal from "../../../../components/DefaulModal";
|
import DefaulModal from "../../../../components/DefaulModal";
|
||||||
import PageLoading from "../../../../components/PageLoading";
|
import PageLoading from "../../../../components/PageLoading";
|
||||||
import { ErrorType } from "../../../../helpers/types";
|
import { ErrorType } from "../../../../helpers/types";
|
||||||
import { useDeleteTask, useGetTaskDetail, useUpdateTask } from "../../task/hooks/useTaskData";
|
import { useChangeTaskPhase, useDeleteTask, useGetTaskDetail, useUpdateTask } from "../../task/hooks/useTaskData";
|
||||||
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
||||||
import type { Task } from "../../types";
|
import type { Column, Task } from "../../types";
|
||||||
import AttachmentList from "./attachment/List";
|
import AttachmentList from "./attachment/List";
|
||||||
import CheckLists from "./checklist/List";
|
import CheckLists from "./checklist/List";
|
||||||
import TaskDetailDescription from "./TaskDetailDescription";
|
import TaskDetailDescription from "./TaskDetailDescription";
|
||||||
@@ -17,11 +17,13 @@ import type { TaskDetailTab } from "./types";
|
|||||||
type Props = {
|
type Props = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
task: Task | null;
|
task: Task | null;
|
||||||
statusLabel: string;
|
columns: Column[];
|
||||||
|
projectId: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onTaskPhaseChanged?: (taskId: string, taskPhaseId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailModal: FC<Props> = ({ open, task, statusLabel, onClose }) => {
|
const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, onTaskPhaseChanged }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const [activeTab, setActiveTab] = useState<TaskDetailTab | null>(null);
|
const [activeTab, setActiveTab] = useState<TaskDetailTab | null>(null);
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
@@ -31,6 +33,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, statusLabel, onClose }) => {
|
|||||||
|
|
||||||
const getTaskDetail = useGetTaskDetail(task?.id ?? "", open && !!task?.id);
|
const getTaskDetail = useGetTaskDetail(task?.id ?? "", open && !!task?.id);
|
||||||
const updateTask = useUpdateTask();
|
const updateTask = useUpdateTask();
|
||||||
|
const changeTaskPhase = useChangeTaskPhase();
|
||||||
const deleteTask = useDeleteTask();
|
const deleteTask = useDeleteTask();
|
||||||
|
|
||||||
const rawTaskDetail = getTaskDetail.data?.data?.task ?? getTaskDetail.data?.data;
|
const rawTaskDetail = getTaskDetail.data?.data?.task ?? getTaskDetail.data?.data;
|
||||||
@@ -61,12 +64,16 @@ const TaskDetailModal: FC<Props> = ({ open, task, statusLabel, onClose }) => {
|
|||||||
initialDescriptionRef.current = taskDetail.description ?? "";
|
initialDescriptionRef.current = taskDetail.description ?? "";
|
||||||
}, [taskDetail]);
|
}, [taskDetail]);
|
||||||
|
|
||||||
|
if (!task) return null;
|
||||||
|
|
||||||
|
const selectedPhaseId = taskDetail?.taskPhaseId ?? task.columnId;
|
||||||
|
|
||||||
const handleTabChange = (tab: TaskDetailTab) => {
|
const handleTabChange = (tab: TaskDetailTab) => {
|
||||||
setActiveTab((prev) => (prev === tab ? null : tab));
|
setActiveTab((prev) => (prev === tab ? null : tab));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (!task?.id) return;
|
if (!task.id) return;
|
||||||
|
|
||||||
deleteTask.mutate(
|
deleteTask.mutate(
|
||||||
{ id: task.id, projectId: taskDetail?.projectId ?? "" },
|
{ id: task.id, projectId: taskDetail?.projectId ?? "" },
|
||||||
@@ -83,8 +90,6 @@ const TaskDetailModal: FC<Props> = ({ open, task, statusLabel, onClose }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveField = (field: "title" | "description", value: string) => {
|
const saveField = (field: "title" | "description", value: string) => {
|
||||||
if (!task?.id) return;
|
|
||||||
|
|
||||||
const initialValue = field === "title" ? initialTitleRef.current : initialDescriptionRef.current;
|
const initialValue = field === "title" ? initialTitleRef.current : initialDescriptionRef.current;
|
||||||
if (value === initialValue) return;
|
if (value === initialValue) return;
|
||||||
|
|
||||||
@@ -111,7 +116,22 @@ const TaskDetailModal: FC<Props> = ({ open, task, statusLabel, onClose }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!task) return null;
|
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 (
|
return (
|
||||||
<DefaulModal open={open} close={onClose} width={680}>
|
<DefaulModal open={open} close={onClose} width={680}>
|
||||||
@@ -120,7 +140,10 @@ const TaskDetailModal: FC<Props> = ({ open, task, statusLabel, onClose }) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<TaskDetailHeader
|
<TaskDetailHeader
|
||||||
statusLabel={statusLabel}
|
columns={columns}
|
||||||
|
selectedPhaseId={selectedPhaseId}
|
||||||
|
onPhaseChange={handlePhaseChange}
|
||||||
|
isChangingPhase={changeTaskPhase.isPending}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
isDeleting={deleteTask.isPending}
|
isDeleting={deleteTask.isPending}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/TaskPhaseService";
|
import * as api from "../service/TaskPhaseService";
|
||||||
import { CreateTaskPhaseType } from "../types/TaskPhaseTypes";
|
import { CreateTaskPhaseType, UpdateTaskPhaseType } from "../types/TaskPhaseTypes";
|
||||||
|
|
||||||
export const useCreateTaskPhase = () => {
|
export const useCreateTaskPhase = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -16,6 +16,18 @@ export const useCreateTaskPhase = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdateTaskPhase = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, params }: { id: string; params: UpdateTaskPhaseType; projectId: string }) =>
|
||||||
|
api.updateTaskPhase(id, params),
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project", variables.projectId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useDeleteTaskPhase = () => {
|
export const useDeleteTaskPhase = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import axios from "../../../../config/axios";
|
import axios from "../../../../config/axios";
|
||||||
import { CreateTaskPhaseType } from "../types/TaskPhaseTypes";
|
import { CreateTaskPhaseType, UpdateTaskPhaseType } from "../types/TaskPhaseTypes";
|
||||||
|
|
||||||
export const createTaskPhase = async (params: CreateTaskPhaseType) => {
|
export const createTaskPhase = async (params: CreateTaskPhaseType) => {
|
||||||
const { data } = await axios.post(`/task-manager/task-phases`, params);
|
const { data } = await axios.post(`/task-manager/task-phases`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateTaskPhase = async (id: string, params: UpdateTaskPhaseType) => {
|
||||||
|
const { data } = await axios.patch(`/task-manager/task-phases/${id}`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteTaskPhase = async (id: string) => {
|
export const deleteTaskPhase = async (id: string) => {
|
||||||
const { data } = await axios.delete(`/task-manager/task-phases/${id}`);
|
const { data } = await axios.delete(`/task-manager/task-phases/${id}`);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ export type CreateTaskPhaseType = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateTaskPhaseType = {
|
||||||
|
order: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type TaskPhaseItemType = {
|
export type TaskPhaseItemType = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/TaskService";
|
import * as api from "../service/TaskService";
|
||||||
import {
|
import {
|
||||||
|
ChangeTaskPhaseType,
|
||||||
CreateCheckListItemType,
|
CreateCheckListItemType,
|
||||||
CreateTaskAttachmentType,
|
CreateTaskAttachmentType,
|
||||||
CreateTaskRemarkType,
|
CreateTaskRemarkType,
|
||||||
@@ -40,6 +41,20 @@ export const useUpdateTask = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useChangeTaskPhase = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, params }: { id: string; params: ChangeTaskPhaseType; projectId: string }) =>
|
||||||
|
api.changeTaskPhase(id, params),
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.id] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project", variables.projectId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useDeleteTask = () => {
|
export const useDeleteTask = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios from "../../../../config/axios";
|
import axios from "../../../../config/axios";
|
||||||
import {
|
import {
|
||||||
|
ChangeTaskPhaseType,
|
||||||
CreateCheckListItemResponse,
|
CreateCheckListItemResponse,
|
||||||
CreateCheckListItemType,
|
CreateCheckListItemType,
|
||||||
CreateTaskAttachmentType,
|
CreateTaskAttachmentType,
|
||||||
@@ -27,6 +28,11 @@ export const updateTask = async (id: string, params: UpdateTaskType) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const changeTaskPhase = async (id: string, params: ChangeTaskPhaseType) => {
|
||||||
|
const { data } = await axios.patch(`/task-manager/tasks/${id}/change-phase`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteTask = async (id: string) => {
|
export const deleteTask = async (id: string) => {
|
||||||
const { data } = await axios.delete(`/task-manager/tasks/${id}`);
|
const { data } = await axios.delete(`/task-manager/tasks/${id}`);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ export type TaskDetailType = {
|
|||||||
attachments?: TaskAttachmentType[];
|
attachments?: TaskAttachmentType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ChangeTaskPhaseType = {
|
||||||
|
taskPhaseId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateTaskType = Partial<
|
export type UpdateTaskType = Partial<
|
||||||
Pick<TaskDetailType, "title" | "description" | "taskPhaseId" | "order" | "startDate" | "endDate">
|
Pick<TaskDetailType, "title" | "description" | "taskPhaseId" | "order" | "startDate" | "endDate">
|
||||||
> & {
|
> & {
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ export const reorderColumns = (
|
|||||||
const result = [...columns];
|
const result = [...columns];
|
||||||
const [removed] = result.splice(oldIndex, 1);
|
const [removed] = result.splice(oldIndex, 1);
|
||||||
result.splice(newIndex, 0, removed);
|
result.splice(newIndex, 0, removed);
|
||||||
return result;
|
return result.map((column, index) => ({ ...column, order: index + 1 }));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { useEffect, useMemo, useState, type FC } from "react";
|
import { useEffect, useMemo, useState, type FC } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import PageLoading from "../../components/PageLoading";
|
import PageLoading from "../../components/PageLoading";
|
||||||
|
import { ErrorType } from "../../helpers/types";
|
||||||
import AddNewColumn from "./components/AddNewColumn";
|
import AddNewColumn from "./components/AddNewColumn";
|
||||||
import Column from "./components/Column";
|
import Column from "./components/Column";
|
||||||
import HeaderWorkspace from "./components/HeaderWorkspace";
|
import HeaderWorkspace from "./components/HeaderWorkspace";
|
||||||
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
|
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
|
||||||
import { useGetProject } from "./project/hooks/useProjectData";
|
import { useGetProject } from "./project/hooks/useProjectData";
|
||||||
|
import { useUpdateTaskPhase } from "./task-phase/hooks/useTaskPhaseData";
|
||||||
|
import { useChangeTaskPhase } from "./task/hooks/useTaskData";
|
||||||
import { mapTaskItemToTask } from "./task/utils/mapTaskItemToTask";
|
import { mapTaskItemToTask } from "./task/utils/mapTaskItemToTask";
|
||||||
import type { TaskItemType } from "./task/types/TaskTypes";
|
import type { TaskItemType } from "./task/types/TaskTypes";
|
||||||
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
|
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
|
||||||
@@ -16,6 +20,8 @@ import { reorderTasks } from "./utils/reorderTasks";
|
|||||||
const Workspace: FC = () => {
|
const Workspace: FC = () => {
|
||||||
const { slug: projectId = "" } = useParams<{ slug: string }>();
|
const { slug: projectId = "" } = useParams<{ slug: string }>();
|
||||||
const { data: project, isPending } = useGetProject(projectId);
|
const { data: project, isPending } = useGetProject(projectId);
|
||||||
|
const changeTaskPhase = useChangeTaskPhase();
|
||||||
|
const updateTaskPhase = useUpdateTaskPhase();
|
||||||
const [columns, setColumns] = useState<ColumnType[]>([]);
|
const [columns, setColumns] = useState<ColumnType[]>([]);
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
|
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
|
||||||
@@ -66,8 +72,48 @@ const Workspace: FC = () => {
|
|||||||
|
|
||||||
const handleDrop = (columnId: string, index: number) => {
|
const handleDrop = (columnId: string, index: number) => {
|
||||||
if (!draggedTaskId) return;
|
if (!draggedTaskId) return;
|
||||||
setTasks((prev) => reorderTasks(prev, draggedTaskId, columnId, index));
|
|
||||||
|
const draggedTask = tasks.find((task) => task.id === draggedTaskId);
|
||||||
|
if (!draggedTask) return;
|
||||||
|
|
||||||
|
const previousTasks = tasks;
|
||||||
|
const columnChanged = draggedTask.columnId !== columnId;
|
||||||
|
const nextTasks = reorderTasks(tasks, draggedTaskId, columnId, index);
|
||||||
|
const movedTask = nextTasks.find((task) => task.id === draggedTaskId) ?? null;
|
||||||
|
|
||||||
|
setTasks(nextTasks);
|
||||||
setDraggedTaskId(null);
|
setDraggedTaskId(null);
|
||||||
|
|
||||||
|
if (selectedTask?.id === draggedTaskId && movedTask) {
|
||||||
|
setSelectedTask(movedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!columnChanged) return;
|
||||||
|
|
||||||
|
changeTaskPhase.mutate(
|
||||||
|
{ id: draggedTaskId, params: { taskPhaseId: columnId }, projectId },
|
||||||
|
{
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
setTasks(previousTasks);
|
||||||
|
if (selectedTask?.id === draggedTaskId) {
|
||||||
|
setSelectedTask(draggedTask);
|
||||||
|
}
|
||||||
|
toast.error(error.response?.data?.error.message[0]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTaskPhaseChanged = (taskId: string, taskPhaseId: string) => {
|
||||||
|
setTasks((prev) => {
|
||||||
|
const task = prev.find((item) => item.id === taskId);
|
||||||
|
if (!task || task.columnId === taskPhaseId) return prev;
|
||||||
|
|
||||||
|
const destinationIndex = prev.filter((item) => item.columnId === taskPhaseId).length;
|
||||||
|
return reorderTasks(prev, taskId, taskPhaseId, destinationIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedTask((prev) => (prev?.id === taskId ? { ...prev, columnId: taskPhaseId } : prev));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleColumnDragStart = (columnId: string) => {
|
const handleColumnDragStart = (columnId: string) => {
|
||||||
@@ -80,20 +126,37 @@ const Workspace: FC = () => {
|
|||||||
|
|
||||||
const handleColumnDrop = (overColumnId: string) => {
|
const handleColumnDrop = (overColumnId: string) => {
|
||||||
if (!draggedColumnId || draggedColumnId === overColumnId) return;
|
if (!draggedColumnId || draggedColumnId === overColumnId) return;
|
||||||
setColumns((prev) => reorderColumns(prev, draggedColumnId, overColumnId));
|
|
||||||
|
const previousColumns = columns;
|
||||||
|
const nextColumns = reorderColumns(columns, draggedColumnId, overColumnId);
|
||||||
|
const movedColumn = nextColumns.find((column) => column.id === draggedColumnId);
|
||||||
|
|
||||||
|
setColumns(nextColumns);
|
||||||
setDraggedColumnId(null);
|
setDraggedColumnId(null);
|
||||||
|
|
||||||
|
if (!movedColumn?.order) return;
|
||||||
|
|
||||||
|
updateTaskPhase.mutate(
|
||||||
|
{ id: draggedColumnId, params: { order: movedColumn.order }, projectId },
|
||||||
|
{
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
setColumns(previousColumns);
|
||||||
|
toast.error(error.response?.data?.error.message[0]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nextColumnOrder = columns.length
|
||||||
|
? Math.max(...columns.map((column) => column.order ?? 0)) + 1
|
||||||
|
: 1;
|
||||||
|
|
||||||
const handleColumnCreated = (phase: TaskPhaseItemType) => {
|
const handleColumnCreated = (phase: TaskPhaseItemType) => {
|
||||||
setColumns((prev) => [
|
setColumns((prev) =>
|
||||||
...prev,
|
[...prev, { id: phase.id, title: phase.name, color: phase.color, order: phase.order }].sort(
|
||||||
{
|
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
||||||
id: phase.id,
|
),
|
||||||
title: phase.name,
|
);
|
||||||
color: phase.color,
|
|
||||||
order: phase.order,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleColumnDeleted = (columnId: string) => {
|
const handleColumnDeleted = (columnId: string) => {
|
||||||
@@ -111,8 +174,6 @@ const Workspace: FC = () => {
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedTaskColumn = selectedTask ? columns.find((column) => column.id === selectedTask.columnId) : null;
|
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return <PageLoading />;
|
return <PageLoading />;
|
||||||
}
|
}
|
||||||
@@ -144,7 +205,7 @@ const Workspace: FC = () => {
|
|||||||
|
|
||||||
<AddNewColumn
|
<AddNewColumn
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
nextOrder={columns.length}
|
nextOrder={nextColumnOrder}
|
||||||
onColumnCreated={handleColumnCreated}
|
onColumnCreated={handleColumnCreated}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,8 +214,10 @@ const Workspace: FC = () => {
|
|||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
open={selectedTask !== null}
|
open={selectedTask !== null}
|
||||||
task={selectedTask}
|
task={selectedTask}
|
||||||
statusLabel={selectedTaskColumn?.title ?? ""}
|
columns={columns}
|
||||||
|
projectId={projectId}
|
||||||
onClose={() => setSelectedTask(null)}
|
onClose={() => setSelectedTask(null)}
|
||||||
|
onTaskPhaseChanged={handleTaskPhaseChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user