fix users list
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-07-18 11:44:14 +03:30
parent ac3e8f5af9
commit 1d56aae0cb
7 changed files with 151 additions and 34 deletions
@@ -1,25 +1,44 @@
import { InfoCircle, Setting2, UserAdd } from "iconsax-react"; import { InfoCircle, Setting2, UserAdd } from "iconsax-react";
import type { FC } from "react"; import type { FC } from "react";
import { useNavigate } from "react-router-dom";
import Button from "../../../components/Button";
type HeaderWorkspaceProps = { type HeaderWorkspaceProps = {
projectName: string; projectName: string;
}; };
const HeaderWorkspace: FC<HeaderWorkspaceProps> = ({ projectName }) => { const HeaderWorkspace: FC<HeaderWorkspaceProps> = ({ projectName }) => {
const navigate = useNavigate();
return ( return (
<div className="flex justify-between items-center bg-primary h-14 sm:h-16 xl:h-[102px] px-3 sm:px-6 xl:px-8 shrink-0 gap-3"> <div className="flex justify-between items-center bg-primary h-14 sm:h-16 xl:h-[102px] px-3 sm:px-6 xl:px-8 shrink-0 gap-3">
<div className="flex gap-2 sm:gap-4 items-center min-w-0"> <div className="flex gap-2 sm:gap-4 items-center min-w-0">
<div className="text-white text-base sm:text-lg xl:text-xl truncate">{projectName}</div> <div className="text-white text-base sm:text-lg xl:text-xl truncate">{projectName}</div>
<InfoCircle size={18} color="white" /> <InfoCircle
size={18}
color="white"
/>
</div> </div>
<div className="flex gap-2 sm:gap-4 items-center shrink-0"> <div className="flex gap-2 sm:gap-4 items-center shrink-0">
<Button
onClick={() => navigate(-1)}
label="برگشت"
/>
<div className="flex gap-1 items-center px-2 sm:px-3 rounded-lg bg-white h-8"> <div className="flex gap-1 items-center px-2 sm:px-3 rounded-lg bg-white h-8">
<UserAdd size={18} color="black" /> <UserAdd
<div className="text-black text-xs hidden sm:block">اشتراک گذاری</div> size={18}
color="black"
/>
<div className="text-black text-xs hidden sm:block whitespace-nowrap">اشتراک گذاری</div>
</div> </div>
<Setting2 size={18} color="white" /> <Setting2
size={18}
color="white"
className="min-w-[18px]"
/>
</div> </div>
</div> </div>
); );
@@ -1,7 +1,7 @@
import { type FC, useEffect, useMemo, useState } from "react"; import { type FC, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useGetUsers } from "../../../users/hooks/useUserData"; import { useGetUsersInfinite } from "../../../users/hooks/useUserData";
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover"; import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
import TaskDetailDatePopover from "./date/TaskDetailDatePopover"; import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
import { parseApiDate, toApiDate } from "./date/utils"; import { parseApiDate, toApiDate } from "./date/utils";
@@ -35,7 +35,16 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
const createTaskAttachment = useCreateTaskAttachment(); const createTaskAttachment = useCreateTaskAttachment();
const singleUpload = useSingleUpload(); const singleUpload = useSingleUpload();
const updateTask = useUpdateTask(); 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<LabelsView>("list"); const [labelsView, setLabelsView] = useState<LabelsView>("list");
const [labels, setLabels] = useState<TaskLabel[]>([]); const [labels, setLabels] = useState<TaskLabel[]>([]);
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set()); const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
@@ -43,6 +52,14 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
const [startDate, setStartDate] = useState<DateObject | null>(null); const [startDate, setStartDate] = useState<DateObject | null>(null);
const [endDate, setEndDate] = useState<DateObject | null>(null); const [endDate, setEndDate] = useState<DateObject | null>(null);
useEffect(() => {
const timeoutId = window.setTimeout(() => {
setDebouncedUserSearch(userSearch.trim());
}, 300);
return () => window.clearTimeout(timeoutId);
}, [userSearch]);
useEffect(() => { useEffect(() => {
if (!taskDetail) return; if (!taskDetail) return;
@@ -69,10 +86,13 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id)); const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id));
const users = useMemo((): TaskUser[] => { const users = useMemo((): TaskUser[] => {
const apiUsers: TaskUser[] = const apiUsers: TaskUser[] =
getUsers.data?.data?.users?.map((user: UserItemType) => ({ usersData?.pages.flatMap(
(page) =>
page.data?.users?.map((user: UserItemType) => ({
id: user.id, id: user.id,
name: `${user.firstName} ${user.lastName}`.trim(), name: `${user.firstName} ${user.lastName}`.trim(),
})) ?? []; })) ?? [],
) ?? [];
const taskUsers: TaskUser[] = const taskUsers: TaskUser[] =
taskDetail?.users?.map((user) => ({ taskDetail?.users?.map((user) => ({
id: user.id, id: user.id,
@@ -87,10 +107,20 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
}); });
return Array.from(userMap.values()); 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 selectedUsers = users.filter((user) => selectedUserIds.has(user.id));
const isLabelsOpen = activeTab === "labels"; const isLabelsOpen = activeTab === "labels";
const isUsersOpen = activeTab === "users";
const isChecklistOpen = activeTab === "checklist"; const isChecklistOpen = activeTab === "checklist";
const isAttachmentOpen = activeTab === "attachment"; const isAttachmentOpen = activeTab === "attachment";
const isDateOpen = activeTab === "date"; const isDateOpen = activeTab === "date";
@@ -305,11 +335,18 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
usersPopover={ usersPopover={
isUsersOpen ? ( isUsersOpen ? (
<TaskDetailUsersPopover <TaskDetailUsersPopover
users={users} users={listUsers}
selectedIds={selectedUserIds} selectedIds={selectedUserIds}
onToggle={handleUserToggle} onToggle={handleUserToggle}
isLoading={getUsers.isPending} isLoading={isUsersPending}
isSubmitting={updateTask.isPending} isSubmitting={updateTask.isPending}
hasNextPage={hasNextUsersPage}
isFetchingNextPage={isFetchingNextUsersPage}
onLoadMore={() => {
void fetchNextUsersPage();
}}
search={userSearch}
onSearchChange={setUserSearch}
/> />
) : null ) : null
} }
@@ -1,4 +1,4 @@
import { type FC, useMemo, useState } from "react"; import { type FC } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Input from "../../../../../components/Input"; import Input from "../../../../../components/Input";
import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox"; import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox";
@@ -10,17 +10,35 @@ type Props = {
onToggle: (id: string) => void; onToggle: (id: string) => void;
isLoading?: boolean; isLoading?: boolean;
isSubmitting?: boolean; isSubmitting?: boolean;
hasNextPage?: boolean;
isFetchingNextPage?: boolean;
onLoadMore?: () => void;
search: string;
onSearchChange: (value: string) => void;
}; };
const TaskDetailUsersList: FC<Props> = ({ users, selectedIds, onToggle, isLoading = false, isSubmitting = false }) => { const TaskDetailUsersList: FC<Props> = ({
users,
selectedIds,
onToggle,
isLoading = false,
isSubmitting = false,
hasNextPage = false,
isFetchingNextPage = false,
onLoadMore,
search,
onSearchChange,
}) => {
const { t } = useTranslation("global"); const { t } = useTranslation("global");
const [search, setSearch] = useState("");
const filteredUsers = useMemo(() => { const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
const query = search.trim().toLowerCase(); const target = event.currentTarget;
if (!query) return users; const isNearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 80;
return users.filter((user) => user.name.toLowerCase().includes(query));
}, [users, search]); if (isNearBottom && hasNextPage && !isFetchingNextPage) {
onLoadMore?.();
}
};
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
@@ -28,7 +46,13 @@ const TaskDetailUsersList: FC<Props> = ({ users, selectedIds, onToggle, isLoadin
<h3 className="text-xs font-bold text-center">{t("taskmanager.users")}</h3> <h3 className="text-xs font-bold text-center">{t("taskmanager.users")}</h3>
</div> </div>
<Input label={t("search")} value={search} onChange={(e) => setSearch(e.target.value)} className="h-8 bg-white/63 border-white" labelClassName="text-xs" /> <Input
label={t("search")}
value={search}
onChange={(e) => onSearchChange(e.target.value)}
className="h-8 bg-white/63 border-white"
labelClassName="text-xs"
/>
<div> <div>
<div className="text-xs font-medium mb-2">{t("taskmanager.task_detail.members")}</div> <div className="text-xs font-medium mb-2">{t("taskmanager.task_detail.members")}</div>
@@ -36,8 +60,8 @@ const TaskDetailUsersList: FC<Props> = ({ users, selectedIds, onToggle, isLoadin
{isLoading ? ( {isLoading ? (
<div className="text-[10px] text-description text-center py-2">{t("loading")}</div> <div className="text-[10px] text-description text-center py-2">{t("loading")}</div>
) : ( ) : (
<div className="flex flex-col gap-2 max-h-48 overflow-y-auto"> <div className="flex flex-col gap-2 max-h-48 overflow-y-auto" onScroll={handleScroll}>
{filteredUsers.map((user) => { {users.map((user) => {
const isSelected = selectedIds.has(user.id); const isSelected = selectedIds.has(user.id);
return ( return (
@@ -52,7 +76,8 @@ const TaskDetailUsersList: FC<Props> = ({ users, selectedIds, onToggle, isLoadin
</div> </div>
); );
})} })}
{!filteredUsers.length ? <div className="text-[10px] text-description">{t("taskmanager.no_users_added")}</div> : null} {isFetchingNextPage ? <div className="text-[10px] text-description text-center py-1">{t("loading")}</div> : null}
{!users.length ? <div className="text-[10px] text-description">{t("taskmanager.no_users_added")}</div> : null}
</div> </div>
)} )}
</div> </div>
@@ -8,9 +8,25 @@ type Props = {
onToggle: (id: string) => void; onToggle: (id: string) => void;
isLoading?: boolean; isLoading?: boolean;
isSubmitting?: boolean; isSubmitting?: boolean;
hasNextPage?: boolean;
isFetchingNextPage?: boolean;
onLoadMore?: () => void;
search: string;
onSearchChange: (value: string) => void;
}; };
const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle, isLoading, isSubmitting }) => { const TaskDetailUsersPopover: FC<Props> = ({
users,
selectedIds,
onToggle,
isLoading,
isSubmitting,
hasNextPage,
isFetchingNextPage,
onLoadMore,
search,
onSearchChange,
}) => {
return ( return (
<TaskDetailUsersList <TaskDetailUsersList
users={users} users={users}
@@ -18,6 +34,11 @@ const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle, isLoa
onToggle={onToggle} onToggle={onToggle}
isLoading={isLoading} isLoading={isLoading}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onLoadMore={onLoadMore}
search={search}
onSearchChange={onSearchChange}
/> />
); );
}; };
+1 -1
View File
@@ -18,7 +18,7 @@ const UsersList: FC = () => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const [page, setPage] = useState<number>(1) const [page, setPage] = useState<number>(1)
const [search, setSearch] = useState<string>('') const [search, setSearch] = useState<string>('')
const getUsers = useGetUsers(search) const getUsers = useGetUsers(search, undefined, page)
const deleteUser = useDeleteUser() const deleteUser = useDeleteUser()
const handleDeleteUser = (id: string) => { const handleDeleteUser = (id: string) => {
+19 -4
View File
@@ -1,5 +1,5 @@
import * as api from "../service/UserService"; import * as api from "../service/UserService";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
import { import {
CreateGroupType, CreateGroupType,
CreateRoleType, 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({ return useQuery({
queryKey: ["users", search, permission], queryKey: ["users", search, permission, page],
queryFn: () => api.getUsers(search, permission), 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,
}); });
}; };
+2 -2
View File
@@ -35,8 +35,8 @@ export const createUser = async (params: CreateUserType) => {
return data; return data;
}; };
export const getUsers = async (search: string, permission?: string) => { export const getUsers = async (search: string, permission?: string, page = 1) => {
let query = `?q=${search}`; let query = `?q=${search}&page=${page}&limit=10`;
if (permission) query += `&permission=${permission}`; if (permission) query += `&permission=${permission}`;
const { data } = await axios.get(`/users/admins${query}`); const { data } = await axios.get(`/users/admins${query}`);
return data; return data;