cursor pagination
This commit is contained in:
@@ -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'
|
||||||
@@ -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,8 +244,13 @@ 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"
|
||||||
|
onScroll={handleTasksScroll}
|
||||||
|
>
|
||||||
|
{isTasksPending
|
||||||
|
? null
|
||||||
|
: tasks.map((task, index) => (
|
||||||
<div
|
<div
|
||||||
key={task.id}
|
key={task.id}
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => {
|
||||||
@@ -245,6 +269,9 @@ const Column: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{isFetchingNextPage ? (
|
||||||
|
<div className="text-center text-description text-xs py-2">{t("loading")}</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAdding ? (
|
{isAdding ? (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user