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 type { FC } from "react";
import { useNavigate } from "react-router-dom";
import Button from "../../../components/Button";
type HeaderWorkspaceProps = {
projectName: string;
};
const HeaderWorkspace: FC<HeaderWorkspaceProps> = ({ projectName }) => {
const navigate = useNavigate();
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 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>
<InfoCircle size={18} color="white" />
<InfoCircle
size={18}
color="white"
/>
</div>
<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">
<UserAdd size={18} color="black" />
<div className="text-black text-xs hidden sm:block">اشتراک گذاری</div>
<UserAdd
size={18}
color="black"
/>
<div className="text-black text-xs hidden sm:block whitespace-nowrap">اشتراک گذاری</div>
</div>
<Setting2 size={18} color="white" />
<Setting2
size={18}
color="white"
className="min-w-[18px]"
/>
</div>
</div>
);
@@ -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<Props> = ({ 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<LabelsView>("list");
const [labels, setLabels] = useState<TaskLabel[]>([]);
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 [endDate, setEndDate] = useState<DateObject | null>(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<Props> = ({ 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<Props> = ({ 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<Props> = ({ activeTab, onTabChange, taskDetail, task
usersPopover={
isUsersOpen ? (
<TaskDetailUsersPopover
users={users}
users={listUsers}
selectedIds={selectedUserIds}
onToggle={handleUserToggle}
isLoading={getUsers.isPending}
isLoading={isUsersPending}
isSubmitting={updateTask.isPending}
hasNextPage={hasNextUsersPage}
isFetchingNextPage={isFetchingNextUsersPage}
onLoadMore={() => {
void fetchNextUsersPage();
}}
search={userSearch}
onSearchChange={setUserSearch}
/>
) : null
}
@@ -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<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 [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<HTMLDivElement>) => {
const target = event.currentTarget;
const isNearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 80;
if (isNearBottom && hasNextPage && !isFetchingNextPage) {
onLoadMore?.();
}
};
return (
<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>
</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 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 ? (
<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">
{filteredUsers.map((user) => {
<div className="flex flex-col gap-2 max-h-48 overflow-y-auto" onScroll={handleScroll}>
{users.map((user) => {
const isSelected = selectedIds.has(user.id);
return (
@@ -52,7 +76,8 @@ const TaskDetailUsersList: FC<Props> = ({ users, selectedIds, onToggle, isLoadin
</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>
@@ -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<Props> = ({ users, selectedIds, onToggle, isLoading, isSubmitting }) => {
const TaskDetailUsersPopover: FC<Props> = ({
users,
selectedIds,
onToggle,
isLoading,
isSubmitting,
hasNextPage,
isFetchingNextPage,
onLoadMore,
search,
onSearchChange,
}) => {
return (
<TaskDetailUsersList
users={users}
@@ -18,6 +34,11 @@ const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle, isLoa
onToggle={onToggle}
isLoading={isLoading}
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 [page, setPage] = useState<number>(1)
const [search, setSearch] = useState<string>('')
const getUsers = useGetUsers(search)
const getUsers = useGetUsers(search, undefined, page)
const deleteUser = useDeleteUser()
const handleDeleteUser = (id: string) => {
+19 -4
View File
@@ -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,
});
};
+2 -2
View File
@@ -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;