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
+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 { toast } from "react-toastify";
import PageLoading from "../../components/PageLoading";
@@ -10,21 +11,24 @@ import TaskDetailModal from "./components/task-detail/TaskDetailModal";
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 type { TaskItemType } from "./task/types/TaskTypes";
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
import type { Column as ColumnType, Task } from "./types";
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 queryClient = useQueryClient();
const { slug: projectId = "" } = useParams<{ slug: string }>();
const { data: project, isPending } = useGetProject(projectId);
const changeTaskPhase = useChangeTaskPhase();
const updateTaskPhase = useUpdateTaskPhase();
const [columns, setColumns] = useState<ColumnType[]>([]);
const [tasks, setTasks] = useState<Task[]>([]);
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
const [draggedTask, setDraggedTask] = useState<Task | null>(null);
const [draggedColumnId, setDraggedColumnId] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
@@ -43,60 +47,79 @@ const Workspace: FC = () => {
);
}, [project?.data?.taskPhases]);
useEffect(() => {
if (!project?.data?.taskPhases) return;
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 handleDragStart = (task: Task) => {
setDraggedTask(task);
};
const handleDragEnd = () => {
setDraggedTaskId(null);
setDraggedTask(null);
};
const handleDrop = (columnId: string, index: number) => {
if (!draggedTaskId) return;
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;
const sourceColumnId = draggedTask.columnId;
const columnChanged = sourceColumnId !== columnId;
const activeTask = draggedTask;
setTasks(nextTasks);
setDraggedTaskId(null);
setDraggedTask(null);
if (selectedTask?.id === draggedTaskId && movedTask) {
setSelectedTask(movedTask);
if (!columnChanged) {
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(
{ 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) => {
setTasks(previousTasks);
if (selectedTask?.id === draggedTaskId) {
setSelectedTask(draggedTask);
if (previousSource) {
queryClient.setQueryData(["tasks-by-task-phase", sourceColumnId], previousSource);
}
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]);
},
@@ -105,13 +128,12 @@ const Workspace: FC = () => {
};
const handleTaskPhaseChanged = (taskId: string, taskPhaseId: string) => {
setTasks((prev) => {
const task = prev.find((item) => item.id === taskId);
if (!task || task.columnId === taskPhaseId) return prev;
const sourceColumnId = selectedTask?.columnId;
const destinationIndex = prev.filter((item) => item.columnId === taskPhaseId).length;
return reorderTasks(prev, taskId, taskPhaseId, destinationIndex);
});
if (sourceColumnId) {
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));
};
@@ -161,19 +183,12 @@ const Workspace: FC = () => {
const handleColumnDeleted = (columnId: string) => {
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) {
setSelectedTask(null);
}
};
const handleTaskCreated = (task: TaskItemType, taskPhaseId: string) => {
setTasks((prev) => [
...prev,
mapTaskItemToTask(task, taskPhaseId, prev.filter((item) => item.columnId === taskPhaseId).length),
]);
};
if (isPending) {
return <PageLoading />;
}
@@ -188,8 +203,7 @@ const Workspace: FC = () => {
key={column.id}
column={column}
projectId={projectId}
tasks={tasksByColumn[column.id] ?? []}
draggedTaskId={draggedTaskId}
draggedTaskId={draggedTask?.id ?? null}
draggedColumnId={draggedColumnId}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
@@ -199,7 +213,6 @@ const Workspace: FC = () => {
onColumnDrop={handleColumnDrop}
onTaskClick={setSelectedTask}
onColumnDeleted={handleColumnDeleted}
onTaskCreated={handleTaskCreated}
/>
))}