This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user