diff --git a/src/pages/taskmanager/components/HeaderWorkspace.tsx b/src/pages/taskmanager/components/HeaderWorkspace.tsx index 5a53e68..9c18090 100644 --- a/src/pages/taskmanager/components/HeaderWorkspace.tsx +++ b/src/pages/taskmanager/components/HeaderWorkspace.tsx @@ -1,25 +1,44 @@ import { InfoCircle, Setting2, UserAdd } from "iconsax-react"; import type { FC } from "react"; +import { useNavigate } from "react-router-dom"; +import Button from "../../../components/Button"; type HeaderWorkspaceProps = { projectName: string; }; const HeaderWorkspace: FC = ({ projectName }) => { + const navigate = useNavigate(); + return (
{projectName}
- +
+
); diff --git a/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx b/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx index 7efb61f..11bbed5 100644 --- a/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx +++ b/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx @@ -1,7 +1,7 @@ import { type FC, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; -import { useGetUsers } from "../../../users/hooks/useUserData"; +import { useGetUsersInfinite } from "../../../users/hooks/useUserData"; import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover"; import TaskDetailDatePopover from "./date/TaskDetailDatePopover"; import { parseApiDate, toApiDate } from "./date/utils"; @@ -35,7 +35,16 @@ const TaskDetailToolbar: FC = ({ activeTab, onTabChange, taskDetail, task const createTaskAttachment = useCreateTaskAttachment(); const singleUpload = useSingleUpload(); const updateTask = useUpdateTask(); - const getUsers = useGetUsers(""); + const isUsersOpen = activeTab === "users"; + const [userSearch, setUserSearch] = useState(""); + const [debouncedUserSearch, setDebouncedUserSearch] = useState(""); + const { + data: usersData, + isPending: isUsersPending, + isFetchingNextPage: isFetchingNextUsersPage, + fetchNextPage: fetchNextUsersPage, + hasNextPage: hasNextUsersPage, + } = useGetUsersInfinite(debouncedUserSearch, undefined, isUsersOpen); const [labelsView, setLabelsView] = useState("list"); const [labels, setLabels] = useState([]); const [selectedLabelIds, setSelectedLabelIds] = useState>(new Set()); @@ -43,6 +52,14 @@ const TaskDetailToolbar: FC = ({ activeTab, onTabChange, taskDetail, task const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); + useEffect(() => { + const timeoutId = window.setTimeout(() => { + setDebouncedUserSearch(userSearch.trim()); + }, 300); + + return () => window.clearTimeout(timeoutId); + }, [userSearch]); + useEffect(() => { if (!taskDetail) return; @@ -69,10 +86,13 @@ const TaskDetailToolbar: FC = ({ activeTab, onTabChange, taskDetail, task const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id)); const users = useMemo((): TaskUser[] => { const apiUsers: TaskUser[] = - getUsers.data?.data?.users?.map((user: UserItemType) => ({ - id: user.id, - name: `${user.firstName} ${user.lastName}`.trim(), - })) ?? []; + usersData?.pages.flatMap( + (page) => + page.data?.users?.map((user: UserItemType) => ({ + id: user.id, + name: `${user.firstName} ${user.lastName}`.trim(), + })) ?? [], + ) ?? []; const taskUsers: TaskUser[] = taskDetail?.users?.map((user) => ({ id: user.id, @@ -87,10 +107,20 @@ const TaskDetailToolbar: FC = ({ activeTab, onTabChange, taskDetail, task }); return Array.from(userMap.values()); - }, [getUsers.data, taskDetail?.users]); + }, [usersData?.pages, taskDetail?.users]); + const listUsers = useMemo((): TaskUser[] => { + return ( + usersData?.pages.flatMap( + (page) => + page.data?.users?.map((user: UserItemType) => ({ + id: user.id, + name: `${user.firstName} ${user.lastName}`.trim(), + })) ?? [], + ) ?? [] + ); + }, [usersData?.pages]); const selectedUsers = users.filter((user) => selectedUserIds.has(user.id)); const isLabelsOpen = activeTab === "labels"; - const isUsersOpen = activeTab === "users"; const isChecklistOpen = activeTab === "checklist"; const isAttachmentOpen = activeTab === "attachment"; const isDateOpen = activeTab === "date"; @@ -305,11 +335,18 @@ const TaskDetailToolbar: FC = ({ activeTab, onTabChange, taskDetail, task usersPopover={ isUsersOpen ? ( { + void fetchNextUsersPage(); + }} + search={userSearch} + onSearchChange={setUserSearch} /> ) : null } diff --git a/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersList.tsx b/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersList.tsx index d44b143..33c557d 100644 --- a/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersList.tsx +++ b/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersList.tsx @@ -1,4 +1,4 @@ -import { type FC, useMemo, useState } from "react"; +import { type FC } from "react"; import { useTranslation } from "react-i18next"; import Input from "../../../../../components/Input"; import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox"; @@ -10,17 +10,35 @@ type Props = { onToggle: (id: string) => void; isLoading?: boolean; isSubmitting?: boolean; + hasNextPage?: boolean; + isFetchingNextPage?: boolean; + onLoadMore?: () => void; + search: string; + onSearchChange: (value: string) => void; }; -const TaskDetailUsersList: FC = ({ users, selectedIds, onToggle, isLoading = false, isSubmitting = false }) => { +const TaskDetailUsersList: FC = ({ + users, + selectedIds, + onToggle, + isLoading = false, + isSubmitting = false, + hasNextPage = false, + isFetchingNextPage = false, + onLoadMore, + search, + onSearchChange, +}) => { const { t } = useTranslation("global"); - const [search, setSearch] = useState(""); - const filteredUsers = useMemo(() => { - const query = search.trim().toLowerCase(); - if (!query) return users; - return users.filter((user) => user.name.toLowerCase().includes(query)); - }, [users, search]); + const handleScroll = (event: React.UIEvent) => { + const target = event.currentTarget; + const isNearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 80; + + if (isNearBottom && hasNextPage && !isFetchingNextPage) { + onLoadMore?.(); + } + }; return (
@@ -28,7 +46,13 @@ const TaskDetailUsersList: FC = ({ users, selectedIds, onToggle, isLoadin

{t("taskmanager.users")}

- setSearch(e.target.value)} className="h-8 bg-white/63 border-white" labelClassName="text-xs" /> + onSearchChange(e.target.value)} + className="h-8 bg-white/63 border-white" + labelClassName="text-xs" + />
{t("taskmanager.task_detail.members")}
@@ -36,8 +60,8 @@ const TaskDetailUsersList: FC = ({ users, selectedIds, onToggle, isLoadin {isLoading ? (
{t("loading")}
) : ( -
- {filteredUsers.map((user) => { +
+ {users.map((user) => { const isSelected = selectedIds.has(user.id); return ( @@ -52,7 +76,8 @@ const TaskDetailUsersList: FC = ({ users, selectedIds, onToggle, isLoadin
); })} - {!filteredUsers.length ?
{t("taskmanager.no_users_added")}
: null} + {isFetchingNextPage ?
{t("loading")}
: null} + {!users.length ?
{t("taskmanager.no_users_added")}
: null}
)}
diff --git a/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx b/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx index be820f3..e099b1a 100644 --- a/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx +++ b/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx @@ -8,9 +8,25 @@ type Props = { onToggle: (id: string) => void; isLoading?: boolean; isSubmitting?: boolean; + hasNextPage?: boolean; + isFetchingNextPage?: boolean; + onLoadMore?: () => void; + search: string; + onSearchChange: (value: string) => void; }; -const TaskDetailUsersPopover: FC = ({ users, selectedIds, onToggle, isLoading, isSubmitting }) => { +const TaskDetailUsersPopover: FC = ({ + users, + selectedIds, + onToggle, + isLoading, + isSubmitting, + hasNextPage, + isFetchingNextPage, + onLoadMore, + search, + onSearchChange, +}) => { return ( = ({ users, selectedIds, onToggle, isLoa onToggle={onToggle} isLoading={isLoading} isSubmitting={isSubmitting} + hasNextPage={hasNextPage} + isFetchingNextPage={isFetchingNextPage} + onLoadMore={onLoadMore} + search={search} + onSearchChange={onSearchChange} /> ); }; diff --git a/src/pages/users/List.tsx b/src/pages/users/List.tsx index 4c8ed3a..e8c3038 100644 --- a/src/pages/users/List.tsx +++ b/src/pages/users/List.tsx @@ -18,7 +18,7 @@ const UsersList: FC = () => { const { t } = useTranslation('global') const [page, setPage] = useState(1) const [search, setSearch] = useState('') - const getUsers = useGetUsers(search) + const getUsers = useGetUsers(search, undefined, page) const deleteUser = useDeleteUser() const handleDeleteUser = (id: string) => { diff --git a/src/pages/users/hooks/useUserData.ts b/src/pages/users/hooks/useUserData.ts index ccb28a0..e6b5aa4 100644 --- a/src/pages/users/hooks/useUserData.ts +++ b/src/pages/users/hooks/useUserData.ts @@ -1,5 +1,5 @@ import * as api from "../service/UserService"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query"; import { CreateGroupType, CreateRoleType, @@ -47,10 +47,25 @@ export const useCreateUser = () => { }); }; -export const useGetUsers = (search: string, permission?: string) => { +export const useGetUsers = (search: string, permission?: string, page = 1) => { return useQuery({ - queryKey: ["users", search, permission], - queryFn: () => api.getUsers(search, permission), + queryKey: ["users", search, permission, page], + queryFn: () => api.getUsers(search, permission, page), + }); +}; + +export const useGetUsersInfinite = (search: string, permission?: string, enabled = true) => { + return useInfiniteQuery({ + queryKey: ["users-infinite", search, permission], + queryFn: ({ pageParam = 1 }) => api.getUsers(search, permission, pageParam), + initialPageParam: 1, + getNextPageParam: (lastPage) => { + const currentPage = lastPage.data?.pager?.page; + const totalPages = lastPage.data?.pager?.totalPages; + + return currentPage < totalPages ? currentPage + 1 : undefined; + }, + enabled, }); }; diff --git a/src/pages/users/service/UserService.ts b/src/pages/users/service/UserService.ts index 0c2c5e9..87bfd63 100644 --- a/src/pages/users/service/UserService.ts +++ b/src/pages/users/service/UserService.ts @@ -35,8 +35,8 @@ export const createUser = async (params: CreateUserType) => { return data; }; -export const getUsers = async (search: string, permission?: string) => { - let query = `?q=${search}`; +export const getUsers = async (search: string, permission?: string, page = 1) => { + let query = `?q=${search}&page=${page}&limit=10`; if (permission) query += `&permission=${permission}`; const { data } = await axios.get(`/users/admins${query}`); return data;