Skip to content

Commit

Permalink
Implement grouping-members-table.tsx
Browse files Browse the repository at this point in the history
  • Loading branch information
JorWo committed Feb 3, 2025
1 parent 8ca2647 commit 893d5c8
Show file tree
Hide file tree
Showing 66 changed files with 1,560 additions and 345 deletions.
2 changes: 2 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-query": "^5.62.2",
"@tanstack/react-table": "^8.20.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"dotenv": "^16.4.1",
"lucide-react": "^0.453.0",
"next": "14.2.15",
"next-cas-client": "^1.3.2",
"nuqs": "^2.2.3",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.50.1",
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/(home)/_components/#-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Button } from '@/components/ui/button';
import Role from '@/lib/access/role';
import { login, logout } from 'next-cas-client';
import { User } from '@/lib/access/user';
import User from '@/lib/access/user';

const LoginButton = ({ currentUser }: { currentUser: User }) => (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import GroupingMembersTable, {
GroupingMembersTableSearchParams
} from './grouping-members-table/grouping-members-table';
import SortBy from '@/app/groupings/[groupingPath]/@tab/_components/grouping-members-table/table-element/sort-by';
import { getGroupingMembers } from '@/lib/actions';
import { Group } from '@/lib/types';
import { parseAsBoolean, parseAsInteger, parseAsStringEnum } from 'nuqs/server';

const size = parseInt(process.env.NEXT_PUBLIC_PAGE_SIZE as string);

const GroupingMembersTab = async ({
params,
searchParams,
group
}: {
params: { groupingPath: string };
searchParams: GroupingMembersTableSearchParams;
group?: Group;
}) => {
const groupingPath = decodeURIComponent(params.groupingPath);

const page = parseAsInteger.withDefault(1).parseServerSide(searchParams.page);
const sortBy = parseAsStringEnum<SortBy>(Object.values(SortBy))
.withDefault(SortBy.NAME)
.parseServerSide(searchParams.sortBy);
const isAscending = parseAsBoolean.withDefault(true).parseServerSide(searchParams.isAscending);
const searchString = searchParams.search;

const groupPath = group ? groupingPath + ':' + group : groupingPath;
const groupingGroupMembers = await getGroupingMembers(groupPath, {
page,
size,
sortBy,
isAscending,
searchString
});

return (
<GroupingMembersTable groupingGroupMembers={groupingGroupMembers} groupingPath={groupingPath} group={group} />
);
};

export default GroupingMembersTab;
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Skeleton } from '@/components/ui/skeleton';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import GroupingMembersTableColumns from './table-element/grouping-members-table-columns';
import { Group } from '@/lib/types';

const pageSize = parseInt(process.env.NEXT_PUBLIC_PAGE_SIZE as string);

const GroupingMembersTableSkeleton = ({ group }: { group?: Group }) => {
return (
<div className="px-4 py-1">
<div className="flex flex-col md:flex-row md:justify-between">
<h1 className="font-bold text-[32px] capitalize">{group ?? 'All Members'}</h1>
<div className="md:w-60 lg:w-72">
<Skeleton className="h-10 w-72 rounded" />
</div>
</div>
<Table className="table-fixed mb-4">
<TableHeader>
<TableRow>
{GroupingMembersTableColumns().map((column, index) => (
<TableHead
key={`header-${column}-${index}`}
className={`pl-[0.5rem]
${index > 0 ? 'hidden sm:table-cell' : ''}`}
>
<Skeleton className="h-4 w-36 rounded" />
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{Array.from(Array(pageSize), (_, index) => (
<TableRow key={`row-${index}`}>
{GroupingMembersTableColumns().map((column, index) => (
<TableCell
key={`cell-${column}-${index}`}
className={`p-[0.5rem]
${index > 0 ? 'hidden sm:table-cell' : ''}`}
>
<Skeleton className="h-4 w-48 rounded" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
<div className="float-end">
<Skeleton className="h-10 w-80 rounded" />
</div>
</div>
);
};

export default GroupingMembersTableSkeleton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
'use client';

import {
flexRender,
functionalUpdate,
getCoreRowModel,
PaginationState,
SortingState,
Updater,
useReactTable
} from '@tanstack/react-table';
import GroupingMembersTableColumns from './table-element/grouping-members-table-columns';
import { Group, GroupingGroupMembers } from '@/lib/types';
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '@/components/ui/table';
import SortArrow from '@/components/table/table-element/sort-arrow';
import PaginationBar from '@/components/table/table-element/pagination-bar';
import { useTransition } from 'react';
import GlobalFilter from '@/components/table/table-element/global-filter';
import { useQuery } from '@tanstack/react-query';
import { Spinner } from '@/components/ui/spinner';
import { getGroupingMembersIsBasis, getGroupingMembersWhereListed, getNumberOfGroupingMembers } from '@/lib/actions';
import {
parseAsBoolean,
parseAsInteger,
parseAsString,
parseAsStringEnum,
useQueryState,
UseQueryStateOptions,
useQueryStates
} from 'nuqs';
import SortBy, {
findSortBy
} from '@/app/groupings/[groupingPath]/@tab/_components/grouping-members-table/table-element/sort-by';

const pageSize = parseInt(process.env.NEXT_PUBLIC_PAGE_SIZE as string);

export type GroupingMembersTableSearchParams = {
page: string;
sortBy: string;
isAscending: string;
search?: string;
};

const GroupingMembersTable = ({
groupingGroupMembers,
groupingPath,
group
}: {
groupingGroupMembers: GroupingGroupMembers;
groupingPath: string;
group?: Group;
}) => {
const { members, size } = groupingGroupMembers;
const queryStateOptions: Omit<UseQueryStateOptions<string>, 'parse'> = {
history: 'replace',
scroll: false,
shallow: false
};

// Pagination.
const [page, setPage] = useQueryState('page', parseAsInteger.withOptions(queryStateOptions).withDefault(1));
const pagination: PaginationState = {
pageIndex: page - 1,
pageSize
};
const onPaginationChange = (updater: Updater<PaginationState>) => {
const updatedPagination = functionalUpdate(updater, pagination);
setPage(updatedPagination.pageIndex + 1);
};

// Sorting.
const [sort, setSort] = useQueryStates({
sortBy: parseAsStringEnum<SortBy>(Object.values(SortBy))
.withOptions(queryStateOptions)
.withDefault(SortBy.NAME),
isAscending: parseAsBoolean.withOptions(queryStateOptions).withDefault(true)
});
const sorting: SortingState = [{ id: sort.sortBy, desc: !sort.isAscending }];
const onSortingChange = (updater: Updater<SortingState>) => {
const updatedSorting = functionalUpdate(updater, sorting);
const { id, desc } = updatedSorting[0];
setSort({
sortBy: findSortBy(id),
isAscending: !desc
});
};

// Global Filter.
const [isSearchLoading, startTransition] = useTransition();
const [globalFilter, setGlobalFilter] = useQueryState(
'search',
parseAsString.withOptions({ ...queryStateOptions, startTransition, throttleMs: 200 }).withDefault('')
);
const onGlobalFilterChange = () => {
setPage(1);
};

// Fetch number of grouping members.
const groupPath = group ? groupingPath + ':' + group : groupingPath;
const { data: rowCount = Infinity, isPending: isRowCountPending } = useQuery({
queryKey: [groupPath, 'rowCount'],
queryFn: () => getNumberOfGroupingMembers(groupPath)
});

const uhUuids = members.map((member) => member.uhUuid);

// Fetch isBasis data.
const { data: groupingMembersIsBasis = members, isPending: isBasisPending } = useQuery({
staleTime: Infinity, // The basis group members rarely change.
enabled: members.length > 0 && !['basis', 'owners', undefined].includes(group),
queryKey: [`${groupingPath}:basis`, uhUuids],
queryFn: () => getGroupingMembersIsBasis(groupingPath, uhUuids).then((res) => res.members)
});

// Fetch whereListed data.
const { data: groupingMembersWhereListed = members, isPending: isWhereListedPending } = useQuery({
enabled: members.length > 0 && !group,
queryKey: [groupingPath, uhUuids],
queryFn: () => getGroupingMembersWhereListed(groupingPath, uhUuids).then((res) => res.members)
});

const table = useReactTable({
columns: GroupingMembersTableColumns(group, !group ? isWhereListedPending : isBasisPending),
data: !group ? groupingMembersWhereListed : groupingMembersIsBasis,
rowCount: globalFilter ? size : rowCount,
getCoreRowModel: getCoreRowModel(),
onPaginationChange,
onSortingChange,
state: { pagination, sorting },
manualPagination: true,
manualSorting: true,
enableSortingRemoval: false
});

return (
<div className="px-4 py-1">
<div className="flex flex-col md:flex-row md:justify-between">
<h1 className="flex font-bold text-[32px] capitalize">
{group ?? 'All Members'} {!isRowCountPending ? `(${rowCount})` : <Spinner className="ml-2" />}
</h1>
<div className="md:w-60 lg:w-72">
<GlobalFilter
placeholder="Filter Members..."
filter={globalFilter}
setFilter={setGlobalFilter}
onFilterChange={onGlobalFilterChange}
isLoading={isSearchLoading}
/>
</div>
</div>
<Table className="table-fixed">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
onClick={header.column.getToggleSortingHandler()}
className={`
${!table.getIsAllColumnsVisible() && header.column.getIndex() > 0 ? '' : ''}
${header.column.getIndex() > 0 ? 'hidden sm:table-cell' : 'w-2/5 md:w-1/3'}
`}
>
<div className="flex items-center">
{flexRender(header.column.columnDef.header, header.getContext())}
<SortArrow direction={header.column.getIsSorted()} />
</div>
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`${cell.column.getIndex() > 0 ? 'hidden sm:table-cell' : ''}`}
>
<div className="flex items-center px-5 py-1.5 overflow-hidden whitespace-nowrap">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
<PaginationBar table={table} />
</div>
);
};

export default GroupingMembersTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Skeleton } from '@/components/ui/skeleton';
import { WhereListed } from '@/lib/types';

const GroupingMemberIsBasisCell = ({ whereListed, isPending }: { whereListed: WhereListed; isPending?: boolean }) => {
return <>{!isPending ? <i>{whereListed === 'Basis' ? 'Yes' : 'No'}</i> : <Skeleton className="h-5 w-7" />}</>;
};

export default GroupingMemberIsBasisCell;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { isDepartmental } from '@/lib/access/authorization';
import { faSchool } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';

const GroupingMemberNameCell = ({ name, uid, uhUuid }: { name: string; uid: string; uhUuid: string }) => {
return (
<>
{name}{' '}
{isDepartmental(uid, uhUuid) && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div
className={`ml-1 bg-blue-background rounded-full flex justify-center items-center
h-6 w-6`}
>
<FontAwesomeIcon
icon={faSchool}
size="sm"
aria-label="Departmental Account Icon"
inverse
/>
</div>
</TooltipTrigger>
<TooltipContent className="max-w-52 text-center text-wrap" side="right">
This is a departmental account, not a personal account. Departmental accounts are often
shared by multiple individuals and used for external communications.
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</>
);
};

export default GroupingMemberNameCell;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';

const GroupingMemberUidCell = ({ uid }: { uid: string }) => {
return (
<>
{uid ? (
uid
) : (
<span className="text-text-color">
N/A{' '}
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<FontAwesomeIcon icon={faQuestionCircle} color="black" />
</TooltipTrigger>
<TooltipContent className="max-w-48 text-center whitespace-normal" side="right">
UH Username not available. Either it has not yet been assigned, or the subject is no
longer with UH.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
)}
</>
);
};

export default GroupingMemberUidCell;
Loading

0 comments on commit 893d5c8

Please # to comment.