cursor pagination

This commit is contained in:
hamid zarghami
2026-07-14 14:52:40 +03:30
parent 1d92a954d0
commit a8830ea70d
10 changed files with 294 additions and 99 deletions
+3 -2
View File
@@ -1,4 +1,5 @@
VITE_TOKEN_NAME = 'admin_token' VITE_TOKEN_NAME = 'admin_token'
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token' VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
VITE_BASE_URL = 'https://api.danakcorp.com' #VITE_BASE_URL = 'https://api.danakcorp.com'
#VITE_BASE_URL = 'http://localhost:3500'
VITE_BASE_URL = 'http://10.85.127.88:3500'
+57 -30
View File
@@ -1,11 +1,12 @@
import { Add, AddCircle, CloseCircle } from "iconsax-react"; import { Add, AddCircle, CloseCircle } from "iconsax-react";
import { useRef, useState, type FC } from "react"; import { useMemo, useRef, useState, type FC } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import ModalConfrim from "../../../components/ModalConfrim"; import ModalConfrim from "../../../components/ModalConfrim";
import { ErrorType } from "../../../helpers/types"; import { ErrorType } from "../../../helpers/types";
import { useCreateTask } from "../task/hooks/useTaskData"; import { useCreateTask, useGetTasksByTaskPhase } from "../task/hooks/useTaskData";
import type { TaskItemType } from "../task/types/TaskTypes"; import type { TaskItemType } from "../task/types/TaskTypes";
import { mapTaskItemToTask } from "../task/utils/mapTaskItemToTask";
import { useDeleteTaskPhase } from "../task-phase/hooks/useTaskPhaseData"; import { useDeleteTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
import type { Column as ColumnType, Task as TaskType } from "../types"; import type { Column as ColumnType, Task as TaskType } from "../types";
import ColumnMenu from "./ColumnMenu"; import ColumnMenu from "./ColumnMenu";
@@ -14,10 +15,9 @@ import Task from "./Task";
type Props = { type Props = {
column: ColumnType; column: ColumnType;
projectId: string; projectId: string;
tasks: TaskType[];
draggedTaskId: string | null; draggedTaskId: string | null;
draggedColumnId: string | null; draggedColumnId: string | null;
onDragStart: (taskId: string) => void; onDragStart: (task: TaskType) => void;
onDragEnd: () => void; onDragEnd: () => void;
onDrop: (columnId: string, index: number) => void; onDrop: (columnId: string, index: number) => void;
onColumnDragStart: (columnId: string) => void; onColumnDragStart: (columnId: string) => void;
@@ -25,13 +25,11 @@ type Props = {
onColumnDrop: (overColumnId: string) => void; onColumnDrop: (overColumnId: string) => void;
onTaskClick?: (task: TaskType) => void; onTaskClick?: (task: TaskType) => void;
onColumnDeleted?: (columnId: string) => void; onColumnDeleted?: (columnId: string) => void;
onTaskCreated?: (task: TaskItemType, taskPhaseId: string) => void;
}; };
const Column: FC<Props> = ({ const Column: FC<Props> = ({
column, column,
projectId, projectId,
tasks,
draggedTaskId, draggedTaskId,
draggedColumnId, draggedColumnId,
onDragStart, onDragStart,
@@ -42,11 +40,25 @@ const Column: FC<Props> = ({
onColumnDrop, onColumnDrop,
onTaskClick, onTaskClick,
onColumnDeleted, onColumnDeleted,
onTaskCreated,
}) => { }) => {
const { t } = useTranslation("global"); const { t } = useTranslation("global");
const deleteTaskPhase = useDeleteTaskPhase(); const deleteTaskPhase = useDeleteTaskPhase();
const createTask = useCreateTask(); const createTask = useCreateTask();
const {
data: tasksData,
isPending: isTasksPending,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useGetTasksByTaskPhase(column.id);
const tasks = useMemo(() => {
const items = tasksData?.pages.flatMap((page) => page.data?.items ?? []) ?? [];
return [...items]
.sort((a, b) => (a.order ?? a.priority ?? 0) - (b.order ?? b.priority ?? 0))
.map((task, index) => mapTaskItemToTask(task as TaskItemType, column.id, index));
}, [tasksData?.pages, column.id]);
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(false);
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
@@ -57,6 +69,15 @@ const Column: FC<Props> = ({
const dragImageRef = useRef<HTMLElement | null>(null); const dragImageRef = useRef<HTMLElement | null>(null);
const isDraggingColumn = draggedColumnId === column.id; const isDraggingColumn = draggedColumnId === column.id;
const handleTasksScroll = (event: React.UIEvent<HTMLDivElement>) => {
const target = event.currentTarget;
const isNearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 80;
if (isNearBottom && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
};
const handleColumnDragStart = (e: React.DragEvent) => { const handleColumnDragStart = (e: React.DragEvent) => {
const columnEl = columnRef.current; const columnEl = columnRef.current;
if (columnEl) { if (columnEl) {
@@ -153,9 +174,7 @@ const Column: FC<Props> = ({
projectId, projectId,
}, },
{ {
onSuccess: (data) => { onSuccess: () => {
const task = (data?.data ?? data) as TaskItemType;
onTaskCreated?.(task, column.id);
toast.success(t("success")); toast.success(t("success"));
resetTaskForm(); resetTaskForm();
}, },
@@ -225,26 +244,34 @@ const Column: FC<Props> = ({
label={t("taskmanager.delete_column_confirm")} label={t("taskmanager.delete_column_confirm")}
/> />
<div className="mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-0 overflow-y-auto overscroll-y-contain flex flex-col gap-3 sm:gap-4"> <div
{tasks.map((task, index) => ( className="mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-0 overflow-y-auto overscroll-y-contain flex flex-col gap-3 sm:gap-4"
<div onScroll={handleTasksScroll}
key={task.id} >
onDragOver={(e) => { {isTasksPending
if (draggedColumnId) return; ? null
e.preventDefault(); : tasks.map((task, index) => (
e.stopPropagation(); <div
}} key={task.id}
onDrop={(e) => handleDrop(e, index)} onDragOver={(e) => {
> if (draggedColumnId) return;
<Task e.preventDefault();
task={task} e.stopPropagation();
isDragging={draggedTaskId === task.id} }}
onDragStart={onDragStart} onDrop={(e) => handleDrop(e, index)}
onDragEnd={onDragEnd} >
onClick={onTaskClick} <Task
/> task={task}
</div> isDragging={draggedTaskId === task.id}
))} onDragStart={onDragStart}
onDragEnd={onDragEnd}
onClick={onTaskClick}
/>
</div>
))}
{isFetchingNextPage ? (
<div className="text-center text-description text-xs py-2">{t("loading")}</div>
) : null}
</div> </div>
{isAdding ? ( {isAdding ? (
+2 -2
View File
@@ -5,7 +5,7 @@ import type { Task as TaskType } from "../types";
type Props = { type Props = {
task: TaskType; task: TaskType;
isDragging?: boolean; isDragging?: boolean;
onDragStart: (taskId: string) => void; onDragStart: (task: TaskType) => void;
onDragEnd: () => void; onDragEnd: () => void;
onClick?: (task: TaskType) => void; onClick?: (task: TaskType) => void;
}; };
@@ -23,7 +23,7 @@ const Task: FC<Props> = ({ task, isDragging, onDragStart, onDragEnd, onClick })
draggable draggable
onDragStart={() => { onDragStart={() => {
hasDraggedRef.current = true; hasDraggedRef.current = true;
onDragStart(task.id); onDragStart(task);
}} }}
onDragEnd={() => { onDragEnd={() => {
onDragEnd(); onDragEnd();
@@ -1,4 +1,4 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/TaskService"; import * as api from "../service/TaskService";
import { import {
ChangeTaskPhaseType, ChangeTaskPhaseType,
@@ -18,6 +18,20 @@ export const useGetTaskDetail = (id: string, enabled = true) => {
}); });
}; };
export const useGetTasksByTaskPhase = (taskPhaseId: string, enabled = true) => {
return useInfiniteQuery({
queryKey: ["tasks-by-task-phase", taskPhaseId],
queryFn: ({ pageParam }) => api.getTasksByTaskPhase(taskPhaseId, pageParam),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => {
const pager = lastPage.data?.pager;
if (!pager?.hasNextPage) return undefined;
return pager.nextCursor ?? undefined;
},
enabled: !!taskPhaseId && enabled,
});
};
export const useCreateTask = () => { export const useCreateTask = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -25,6 +39,7 @@ export const useCreateTask = () => {
mutationFn: (variables: CreateTaskType) => api.createTask(variables), mutationFn: (variables: CreateTaskType) => api.createTask(variables),
onSuccess: (_data, variables) => { onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["project", variables.projectId] }); queryClient.invalidateQueries({ queryKey: ["project", variables.projectId] });
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", variables.taskPhaseId] });
}, },
}); });
}; };
@@ -37,6 +52,7 @@ export const useUpdateTask = () => {
onSuccess: (_data, variables) => { onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.id] }); queryClient.invalidateQueries({ queryKey: ["task-detail", variables.id] });
queryClient.invalidateQueries({ queryKey: ["project"] }); queryClient.invalidateQueries({ queryKey: ["project"] });
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] });
}, },
}); });
}; };
@@ -51,6 +67,7 @@ export const useChangeTaskPhase = () => {
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.id] }); queryClient.invalidateQueries({ queryKey: ["task-detail", variables.id] });
queryClient.invalidateQueries({ queryKey: ["project", variables.projectId] }); queryClient.invalidateQueries({ queryKey: ["project", variables.projectId] });
queryClient.invalidateQueries({ queryKey: ["project"] }); queryClient.invalidateQueries({ queryKey: ["project"] });
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", variables.params.taskPhaseId] });
}, },
}); });
}; };
@@ -64,6 +81,7 @@ export const useDeleteTask = () => {
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.id] }); queryClient.invalidateQueries({ queryKey: ["task-detail", variables.id] });
queryClient.invalidateQueries({ queryKey: ["project", variables.projectId] }); queryClient.invalidateQueries({ queryKey: ["project", variables.projectId] });
queryClient.invalidateQueries({ queryKey: ["project"] }); queryClient.invalidateQueries({ queryKey: ["project"] });
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] });
}, },
}); });
}; };
@@ -8,6 +8,7 @@ import {
CreateTaskRemarkType, CreateTaskRemarkType,
CreateTaskType, CreateTaskType,
GetTaskDetailResponse, GetTaskDetailResponse,
GetTasksByTaskPhaseResponse,
UpdateCheckListItemResponse, UpdateCheckListItemResponse,
UpdateCheckListItemType, UpdateCheckListItemType,
UpdateTaskType, UpdateTaskType,
@@ -23,6 +24,20 @@ export const getTaskDetail = async (id: string): Promise<GetTaskDetailResponse>
return data; return data;
}; };
export const getTasksByTaskPhase = async (
taskPhaseId: string,
cursor?: string | null,
limit = 10,
): Promise<GetTasksByTaskPhaseResponse> => {
const params = new URLSearchParams({ limit: String(limit) });
if (cursor) {
params.set("cursor", cursor);
}
const { data } = await axios.get(`/task-manager/tasks/task-phase/${taskPhaseId}?${params.toString()}`);
return data;
};
export const updateTask = async (id: string, params: UpdateTaskType) => { export const updateTask = async (id: string, params: UpdateTaskType) => {
const { data } = await axios.patch(`/task-manager/tasks/${id}`, params); const { data } = await axios.patch(`/task-manager/tasks/${id}`, params);
return data; return data;
+25 -2
View File
@@ -11,15 +11,34 @@ export type TaskItemType = {
id: string; id: string;
title: string; title: string;
taskPhaseId: string; taskPhaseId: string;
order: number; order?: number;
priority?: number;
createdAt?: string;
updatedAt?: string;
description?: string | null;
color?: string | null;
startDate?: string | null;
endDate?: string | null;
tag?: string; tag?: string;
dateRange?: string; dateRange?: string;
color?: string | null;
attachments?: number; attachments?: number;
checklistDone?: number; checklistDone?: number;
checklistTotal?: number; checklistTotal?: number;
}; };
export type TaskPhaseTasksPagerType = {
limit: number;
nextCursor: string | null;
previousCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
};
export type TaskPhaseTasksDataType = {
items: TaskItemType[];
pager: TaskPhaseTasksPagerType;
};
export type TaskLabelType = { export type TaskLabelType = {
id: string; id: string;
title: string; title: string;
@@ -122,3 +141,7 @@ export type UpdateTaskType = Partial<
export interface GetTaskDetailResponse extends IResponse<TaskDetailType> { export interface GetTaskDetailResponse extends IResponse<TaskDetailType> {
statusCode: number; statusCode: number;
} }
export interface GetTasksByTaskPhaseResponse extends IResponse<TaskPhaseTasksDataType> {
statusCode: number;
}
@@ -4,7 +4,7 @@ import type { TaskItemType } from "../types/TaskTypes";
export const mapTaskItemToTask = (task: TaskItemType, taskPhaseId: string, fallbackOrder: number): Task => ({ export const mapTaskItemToTask = (task: TaskItemType, taskPhaseId: string, fallbackOrder: number): Task => ({
id: task.id, id: task.id,
columnId: task.taskPhaseId ?? taskPhaseId, columnId: task.taskPhaseId ?? taskPhaseId,
order: task.order ?? fallbackOrder, order: task.order ?? task.priority ?? fallbackOrder,
title: task.title, title: task.title,
tag: task.tag ?? "", tag: task.tag ?? "",
dateRange: task.dateRange ?? "", dateRange: task.dateRange ?? "",
@@ -0,0 +1,18 @@
import type { TaskItemType } from "../task/types/TaskTypes";
const getTaskSortOrder = (task: TaskItemType) => task.order ?? task.priority ?? 0;
export const reorderTaskItems = (
tasks: TaskItemType[],
taskId: string,
destIndex: number,
): TaskItemType[] => {
const task = tasks.find((item) => item.id === taskId);
if (!task) return tasks;
const without = tasks.filter((item) => item.id !== taskId);
const sorted = [...without].sort((a, b) => getTaskSortOrder(a) - getTaskSortOrder(b));
sorted.splice(destIndex, 0, task);
return sorted.map((item, index) => ({ ...item, order: index, priority: index }));
};
@@ -0,0 +1,80 @@
import type { InfiniteData } from "@tanstack/react-query";
import type { GetTasksByTaskPhaseResponse, TaskItemType } from "../task/types/TaskTypes";
export type TasksByTaskPhaseInfiniteData = InfiniteData<GetTasksByTaskPhaseResponse, string | undefined>;
export const getTaskPhaseItems = (data?: TasksByTaskPhaseInfiniteData): TaskItemType[] =>
data?.pages.flatMap((page) => page.data?.items ?? []) ?? [];
const redistributeItemsAcrossPages = (
pages: GetTasksByTaskPhaseResponse[],
items: TaskItemType[],
): GetTasksByTaskPhaseResponse[] => {
let offset = 0;
return pages.map((page) => {
const pageSize = page.data?.items.length ?? 0;
const pageItems = items.slice(offset, offset + pageSize);
offset += pageSize;
return {
...page,
data: {
...page.data,
items: pageItems,
},
};
});
};
export const updateTaskPhaseItems = (
current: TasksByTaskPhaseInfiniteData | undefined,
updater: (items: TaskItemType[]) => TaskItemType[],
): TasksByTaskPhaseInfiniteData | undefined => {
if (!current?.pages.length) return current;
const updatedItems = updater(getTaskPhaseItems(current));
return {
...current,
pages: redistributeItemsAcrossPages(current.pages, updatedItems),
};
};
export const insertTaskPhaseItem = (
current: TasksByTaskPhaseInfiniteData | undefined,
item: TaskItemType,
index: number,
): TasksByTaskPhaseInfiniteData => {
const items = getTaskPhaseItems(current);
const nextItems = [...items];
nextItems.splice(index, 0, item);
if (!current?.pages.length) {
return {
pages: [
{
status: 200,
success: true,
statusCode: 200,
data: {
items: nextItems,
pager: {
limit: 10,
nextCursor: null,
previousCursor: null,
hasNextPage: false,
hasPreviousPage: false,
},
},
},
],
pageParams: [undefined],
};
}
return {
...current,
pages: redistributeItemsAcrossPages(current.pages, nextItems),
};
};
+74 -61
View File
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState, type FC } from "react"; import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState, type FC } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import PageLoading from "../../components/PageLoading"; import PageLoading from "../../components/PageLoading";
@@ -10,21 +11,24 @@ 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 { useUpdateTaskPhase } from "./task-phase/hooks/useTaskPhaseData";
import { useChangeTaskPhase } from "./task/hooks/useTaskData"; import { useChangeTaskPhase } from "./task/hooks/useTaskData";
import { mapTaskItemToTask } from "./task/utils/mapTaskItemToTask";
import type { TaskItemType } from "./task/types/TaskTypes";
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes"; import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
import type { Column as ColumnType, Task } from "./types"; import type { Column as ColumnType, Task } from "./types";
import { reorderColumns } from "./utils/reorderColumns"; import { reorderColumns } from "./utils/reorderColumns";
import { reorderTasks } from "./utils/reorderTasks"; import { reorderTaskItems } from "./utils/reorderTaskItems";
import {
insertTaskPhaseItem,
type TasksByTaskPhaseInfiniteData,
updateTaskPhaseItems,
} from "./utils/taskPhaseQueryCache";
const Workspace: FC = () => { const Workspace: FC = () => {
const queryClient = useQueryClient();
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 changeTaskPhase = useChangeTaskPhase();
const updateTaskPhase = useUpdateTaskPhase(); const updateTaskPhase = useUpdateTaskPhase();
const [columns, setColumns] = useState<ColumnType[]>([]); const [columns, setColumns] = useState<ColumnType[]>([]);
const [tasks, setTasks] = useState<Task[]>([]); const [draggedTask, setDraggedTask] = useState<Task | null>(null);
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
const [draggedColumnId, setDraggedColumnId] = useState<string | null>(null); const [draggedColumnId, setDraggedColumnId] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null); const [selectedTask, setSelectedTask] = useState<Task | null>(null);
@@ -43,60 +47,79 @@ const Workspace: FC = () => {
); );
}, [project?.data?.taskPhases]); }, [project?.data?.taskPhases]);
useEffect(() => { const handleDragStart = (task: Task) => {
if (!project?.data?.taskPhases) return; setDraggedTask(task);
const apiTasks = project.data.taskPhases.flatMap((phase) =>
(phase.tasks ?? []).map((task, index) =>
mapTaskItemToTask(task as TaskItemType, phase.id, index),
),
);
setTasks(apiTasks);
}, [project?.data?.taskPhases]);
const tasksByColumn = useMemo(() => {
return columns.reduce<Record<string, Task[]>>((acc, column) => {
acc[column.id] = tasks.filter((task) => task.columnId === column.id).sort((a, b) => a.order - b.order);
return acc;
}, {});
}, [columns, tasks]);
const handleDragStart = (taskId: string) => {
setDraggedTaskId(taskId);
}; };
const handleDragEnd = () => { const handleDragEnd = () => {
setDraggedTaskId(null); setDraggedTask(null);
}; };
const handleDrop = (columnId: string, index: number) => { const handleDrop = (columnId: string, index: number) => {
if (!draggedTaskId) return;
const draggedTask = tasks.find((task) => task.id === draggedTaskId);
if (!draggedTask) return; if (!draggedTask) return;
const previousTasks = tasks; const sourceColumnId = draggedTask.columnId;
const columnChanged = draggedTask.columnId !== columnId; const columnChanged = sourceColumnId !== columnId;
const nextTasks = reorderTasks(tasks, draggedTaskId, columnId, index); const activeTask = draggedTask;
const movedTask = nextTasks.find((task) => task.id === draggedTaskId) ?? null;
setTasks(nextTasks); setDraggedTask(null);
setDraggedTaskId(null);
if (selectedTask?.id === draggedTaskId && movedTask) { if (!columnChanged) {
setSelectedTask(movedTask); queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId], (current) =>
updateTaskPhaseItems(current, (items) => reorderTaskItems(items, activeTask.id, index)),
);
return;
} }
if (!columnChanged) return; const previousSource = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>([
"tasks-by-task-phase",
sourceColumnId,
]);
const previousDest = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>([
"tasks-by-task-phase",
columnId,
]);
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", sourceColumnId], (current) =>
updateTaskPhaseItems(current, (items) => items.filter((task) => task.id !== activeTask.id)),
);
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId], (current) =>
insertTaskPhaseItem(current, {
id: activeTask.id,
title: activeTask.title,
taskPhaseId: columnId,
order: index,
priority: index,
tag: activeTask.tag,
dateRange: activeTask.dateRange,
color: activeTask.color,
attachments: activeTask.attachments,
checklistDone: activeTask.checklistDone,
checklistTotal: activeTask.checklistTotal,
}, index),
);
if (selectedTask?.id === activeTask.id) {
setSelectedTask({ ...activeTask, columnId });
}
changeTaskPhase.mutate( changeTaskPhase.mutate(
{ id: draggedTaskId, params: { taskPhaseId: columnId }, projectId }, { id: activeTask.id, params: { taskPhaseId: columnId }, projectId },
{ {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", sourceColumnId] });
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", columnId] });
},
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
setTasks(previousTasks); if (previousSource) {
if (selectedTask?.id === draggedTaskId) { queryClient.setQueryData(["tasks-by-task-phase", sourceColumnId], previousSource);
setSelectedTask(draggedTask); }
if (previousDest) {
queryClient.setQueryData(["tasks-by-task-phase", columnId], previousDest);
}
if (selectedTask?.id === activeTask.id) {
setSelectedTask(activeTask);
} }
toast.error(error.response?.data?.error.message[0]); toast.error(error.response?.data?.error.message[0]);
}, },
@@ -105,13 +128,12 @@ const Workspace: FC = () => {
}; };
const handleTaskPhaseChanged = (taskId: string, taskPhaseId: string) => { const handleTaskPhaseChanged = (taskId: string, taskPhaseId: string) => {
setTasks((prev) => { const sourceColumnId = selectedTask?.columnId;
const task = prev.find((item) => item.id === taskId);
if (!task || task.columnId === taskPhaseId) return prev;
const destinationIndex = prev.filter((item) => item.columnId === taskPhaseId).length; if (sourceColumnId) {
return reorderTasks(prev, taskId, taskPhaseId, destinationIndex); queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", sourceColumnId] });
}); }
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", taskPhaseId] });
setSelectedTask((prev) => (prev?.id === taskId ? { ...prev, columnId: taskPhaseId } : prev)); setSelectedTask((prev) => (prev?.id === taskId ? { ...prev, columnId: taskPhaseId } : prev));
}; };
@@ -161,19 +183,12 @@ const Workspace: FC = () => {
const handleColumnDeleted = (columnId: string) => { const handleColumnDeleted = (columnId: string) => {
setColumns((prev) => prev.filter((column) => column.id !== columnId)); setColumns((prev) => prev.filter((column) => column.id !== columnId));
setTasks((prev) => prev.filter((task) => task.columnId !== columnId)); queryClient.removeQueries({ queryKey: ["tasks-by-task-phase", columnId] });
if (selectedTask?.columnId === columnId) { if (selectedTask?.columnId === columnId) {
setSelectedTask(null); setSelectedTask(null);
} }
}; };
const handleTaskCreated = (task: TaskItemType, taskPhaseId: string) => {
setTasks((prev) => [
...prev,
mapTaskItemToTask(task, taskPhaseId, prev.filter((item) => item.columnId === taskPhaseId).length),
]);
};
if (isPending) { if (isPending) {
return <PageLoading />; return <PageLoading />;
} }
@@ -188,8 +203,7 @@ const Workspace: FC = () => {
key={column.id} key={column.id}
column={column} column={column}
projectId={projectId} projectId={projectId}
tasks={tasksByColumn[column.id] ?? []} draggedTaskId={draggedTask?.id ?? null}
draggedTaskId={draggedTaskId}
draggedColumnId={draggedColumnId} draggedColumnId={draggedColumnId}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
@@ -199,7 +213,6 @@ const Workspace: FC = () => {
onColumnDrop={handleColumnDrop} onColumnDrop={handleColumnDrop}
onTaskClick={setSelectedTask} onTaskClick={setSelectedTask}
onColumnDeleted={handleColumnDeleted} onColumnDeleted={handleColumnDeleted}
onTaskCreated={handleTaskCreated}
/> />
))} ))}