improvement: add pagination to project members table

This commit is contained in:
Scott Wilson
2024-11-27 18:41:20 -08:00
parent c3956c60e9
commit a04834c7c9
3 changed files with 133 additions and 23 deletions
@@ -68,7 +68,7 @@ export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) =>
return membershipOne.project.name return membershipOne.project.name
.toLowerCase() .toLowerCase()
.localeCompare(membershipTwo.project.name.toLowerCase()); .localeCompare(membershipTwo.project.name.toLowerCase());
}) ?? [], }),
[projectMemberships, orderDirection, search] [projectMemberships, orderDirection, search]
); );
@@ -71,7 +71,7 @@ export const UserProjectsTable = ({ membershipId, handlePopUpOpen }: Props) => {
return membershipOne.project.name return membershipOne.project.name
.toLowerCase() .toLowerCase()
.localeCompare(membershipTwo.project.name.toLowerCase()); .localeCompare(membershipTwo.project.name.toLowerCase());
}) ?? [], }),
[projectMemberships, orderDirection, search] [projectMemberships, orderDirection, search]
); );
@@ -1,9 +1,12 @@
import { useMemo, useState } from "react"; import { useMemo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { import {
faArrowDown,
faArrowUp,
faClock, faClock,
faEllipsisV, faEllipsisV,
faMagnifyingGlass, faMagnifyingGlass,
faSearch,
faTrash, faTrash,
faUsers faUsers
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
@@ -18,6 +21,7 @@ import {
HoverCardTrigger, HoverCardTrigger,
IconButton, IconButton,
Input, Input,
Pagination,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -35,7 +39,9 @@ import {
useUser, useUser,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetWorkspaceUsers } from "@app/hooks/api"; import { useGetWorkspaceUsers } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -54,9 +60,12 @@ type Props = {
) => void; ) => void;
}; };
export const MembersTable = ({ handlePopUpOpen }: Props) => { enum MembersOrderBy {
const [searchMemberFilter, setSearchMemberFilter] = useState(""); Name = "firstName",
Email = "email"
}
export const MembersTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { user } = useUser(); const { user } = useUser();
const router = useRouter(); const router = useRouter();
@@ -64,26 +73,77 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
const userId = user?.id || ""; const userId = user?.id || "";
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId); const {
search,
setSearch,
setPage,
page,
perPage,
setPerPage,
offset,
orderDirection,
orderBy,
setOrderBy,
setOrderDirection,
toggleOrderDirection
} = usePagination<MembersOrderBy>(MembersOrderBy.Name);
const filterdUsers = useMemo( const { data: members = [], isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
const filteredUsers = useMemo(
() => () =>
members?.filter( members
({ user: u, inviteEmail }) => ?.filter(
u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || ({ user: u, inviteEmail }) =>
u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.firstName?.toLowerCase().includes(search.toLowerCase()) ||
u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.lastName?.toLowerCase().includes(search.toLowerCase()) ||
u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.username?.toLowerCase().includes(search.toLowerCase()) ||
inviteEmail?.includes(searchMemberFilter.toLowerCase()) u?.email?.toLowerCase().includes(search.toLowerCase()) ||
), inviteEmail?.toLowerCase().includes(search.toLowerCase())
[members, searchMemberFilter] )
.sort((a, b) => {
const [memberOne, memberTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
let valueOne: string;
let valueTwo: string;
switch (orderBy) {
case MembersOrderBy.Email:
valueOne = memberOne.user.email || memberOne.inviteEmail;
valueTwo = memberTwo.user.email || memberTwo.inviteEmail;
break;
case MembersOrderBy.Name:
default:
valueOne = memberOne.user.firstName;
valueTwo = memberTwo.user.firstName;
}
return valueOne.toLowerCase().localeCompare(valueTwo.toLowerCase());
}),
[members, search, orderDirection, orderBy]
); );
useResetPageHelper({
totalCount: filteredUsers.length,
offset,
setPage
});
const handleSort = (column: MembersOrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
return ( return (
<div> <div>
<Input <Input
value={searchMemberFilter} value={search}
onChange={(e) => setSearchMemberFilter(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..." placeholder="Search members..."
/> />
@@ -91,8 +151,44 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
<Th>Name</Th> <Th className="w-1/3">
<Th>Username</Th> <div className="flex items-center">
Name
<IconButton
variant="plain"
className={`ml-2 ${orderBy === MembersOrderBy.Name ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(MembersOrderBy.Name)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC && orderBy === MembersOrderBy.Name
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Email
<IconButton
variant="plain"
className={`ml-2 ${orderBy === MembersOrderBy.Email ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(MembersOrderBy.Email)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC && orderBy === MembersOrderBy.Email
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th>Role</Th> <Th>Role</Th>
<Th className="w-5" /> <Th className="w-5" />
</Tr> </Tr>
@@ -100,7 +196,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
<TBody> <TBody>
{isMembersLoading && <TableSkeleton columns={4} innerKey="project-members" />} {isMembersLoading && <TableSkeleton columns={4} innerKey="project-members" />}
{!isMembersLoading && {!isMembersLoading &&
filterdUsers?.map((projectMember) => { filteredUsers.slice(offset, perPage * page).map((projectMember) => {
const { user: u, inviteEmail, id: membershipId, roles } = projectMember; const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
const name = u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : "-"; const name = u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : "-";
const email = u?.email || inviteEmail; const email = u?.email || inviteEmail;
@@ -239,8 +335,22 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
})} })}
</TBody> </TBody>
</Table> </Table>
{!isMembersLoading && filterdUsers?.length === 0 && ( {Boolean(filteredUsers.length) && (
<EmptyState title="No project members found" icon={faUsers} /> <Pagination
count={filteredUsers.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
/>
)}
{!isMembersLoading && !filteredUsers?.length && (
<EmptyState
title={
members.length ? "No project members match search..." : "No project members found"
}
icon={members.length ? faSearch : faUsers}
/>
)} )}
</TableContainer> </TableContainer>
</div> </div>