Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PersonTable #114

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion ui/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ const nextConfig = {
{
source: '/groupings/:groupingPath',
destination: '/groupings/:groupingPath/all-members'
},
{
source: '/admin',
destination: '/admin/manage-groupings'
}
];
}
};

export default nextConfig;
export default nextConfig;
37 changes: 37 additions & 0 deletions ui/src/app/admin/@tab/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import Link from 'next/link';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { usePathname } from 'next/navigation';

const AdminTabsLayout = ({ children }: { children: React.ReactNode }) => {
const currentPath = usePathname().split('/').pop();
return (
<>
<Tabs className="bg-seafoam" value={currentPath === 'admin' ? 'manage-groupings' : currentPath.toString()}>
<div className="container">
<TabsList variant="outline">
<Link key={'groupings'} href={`/admin/manage-groupings`}>
<TabsTrigger value="manage-groupings" variant="outline">
Manage Groupings
</TabsTrigger>
</Link>
<Link key={'admins'} href={'/admin/manage-admins'}>
<TabsTrigger value="manage-admins" variant="outline">
Manage Admins
</TabsTrigger>
</Link>
<Link key={'person'} href={'/admin/manage-person'}>
<TabsTrigger value="manage-person" variant="outline">
Manage Person
</TabsTrigger>
</Link>
</TabsList>
</div>
</Tabs>
{children}
</>
);
};

export default AdminTabsLayout;
9 changes: 9 additions & 0 deletions ui/src/app/admin/@tab/manage-admins/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const AdminsTab = () => {
return (
<div className="container">
<h1>Testing Admins</h1>
</div>
);
};

export default AdminsTab;
9 changes: 9 additions & 0 deletions ui/src/app/admin/@tab/manage-groupings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const GroupingsTab = () => {
return (
<div className="container">
<h1>Testing Groupings</h1>
</div>
);
};

export default GroupingsTab;
20 changes: 20 additions & 0 deletions ui/src/app/admin/@tab/manage-person/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { managePersonResults } from '@/lib/fetchers';
import PersonTable from '@/app/admin/_components/personTable';
import { memberAttributeResults } from '@/lib/actions';

const PersonTab = async (searchParams) => {
Copy link
Contributor

@JorWo JorWo Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to set a type for searchParams, it would probably be: searchParams: { searchUid: string }. Also I recommend changing the searchParam from searchUid to uhIdentifier because that naming would cover both uid and uhUuid.

const searchUid = searchParams.searchParams.searchUid;
Copy link
Contributor

@JorWo JorWo Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I'm not sure why the searchParams object would make you access .searchParams

const groupingsInfo = await managePersonResults(searchUid);
const userInfo = searchUid === undefined ? undefined : (await memberAttributeResults([searchUid])).results[0];
Comment on lines +7 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call these variables managePersonResults and memberAttributeResults so it's more clear what exactly we are passing into the PersonTable.

const props = { groupingsInfo: groupingsInfo, userInfo: userInfo, searchUid: searchUid };

return (
<>
<div className="container">
<PersonTable {...props} />
Comment on lines +9 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of defining a props object, follow the other components and how they define each prop individually.

</div>
</>
);
};

export default PersonTab;
284 changes: 284 additions & 0 deletions ui/src/app/admin/_components/personTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
'use client';

import {
useReactTable,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getFilteredRowModel,
getSortedRowModel,
SortingState,
RowSelectionState
} from '@tanstack/react-table';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import PaginationBar from '@/components/table/table-element/pagination-bar';
import GlobalFilter from '@/components/table/table-element/global-filter';
import SortArrow from '@/components/table/table-element/sort-arrow';
import { useState } from 'react';
import { ArrowUpRightFromSquare } from 'lucide-react';
import { CrownIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import SearchInput from '@/app/admin/_components/searchInput';
import personTableColumns from '@/components/table/table-element/person-table-columns';
import PersonTableTooltip from '@/app/admin/_components/personTableTooltip';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { groupingOwners, removeFromGroups } from '@/lib/actions';
import OwnersModal from '@/components/modal/owners-modal';
import { Alert, AlertDescription } from '@/components/ui/alert';

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

const PersonTable = (data) => {
const [globalFilter, setGlobalFilter] = useState('');
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [modalOpen, setModalOpen] = useState(false);
const [modalData, setModalData] = useState([]);
const [dummyBool, setDummyBool] = useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this dummyBool for?

const router = useRouter();
const searchUid = data.searchUid;
const validUid = data.groupingsInfo.resultCode;
const groupingsInfo = data.groupingsInfo.results;
const userInfo = data.userInfo;
const hydrateModal = async (path) => {
setModalData((await groupingOwners(path)).members);
setModalOpen(true);
};

const close = () => {
setModalOpen(false);
};

const handleRemove = async () => {
const numSelected = table.getSelectedRowModel().rows.length;
const arr = [];
let i;
for (i = 0; i < numSelected; i++) {
const original = table.getSelectedRowModel().rows[i].original;
if (original.inOwner) arr.push(original.path + ':owners');
if (original.inInclude) arr.push(original.path + ':include');
if (original.inExclude) arr.push(original.path + ':exclude');
}
Comment on lines +54 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a very nice way to rewrite this code using the .flatMap function: (Generated by ChatGPT!)

const groups = table.getSelectedRowModel().rows.flatMap(({ original }) => {
    const { path, inOwner, inInclude, inExclude } = original;
    return [
        inOwner && `${path}:owners`,
        inInclude && `${path}:include`,
        inExclude && `${path}:exclude`,
    ].filter(groupPath => groupPath);
});

await removeFromGroups(searchUid, arr);
setRowSelection({});
router.refresh();
};

const table = useReactTable({
columns: personTableColumns,
data: groupingsInfo,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
state: { globalFilter, sorting, rowSelection },
initialState: { pagination: { pageSize } },
onGlobalFilterChange: setGlobalFilter,
onSortingChange: setSorting,
onRowSelectionChange: setRowSelection
});

return (
<>
<OwnersModal open={modalOpen} close={close} modalData={modalData} />
<div className="flex flex-col md:flex-row md:justify-between pt-1 mb-1">
<div className="flex items-center">
<h1 className="text-[2rem] font-medium text-text-color md:justify-start pt-3">Manage Person</h1>
<p className="text-[1.2rem] font-medium text-text-color md:justify-end pt-5 ps-2">
{userInfo === undefined
? ''
: '(' + userInfo.name + ', ' + userInfo.uid + ', ' + userInfo.uhUuid + ')'}
</p>
Comment on lines +88 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of the ternary operator, it's nicer to use the && operator:

{userInfo !== undefined && <p className="text-[1.2rem] font-medium text-text-color md:justify-end pt-5 ps-2">
    {'(' + userInfo.name + ', ' + userInfo.uid + ', ' + userInfo.uhUuid + ')'}
</p>}

</div>
<div className="flex flex-col md:flex-row md:justify-end md:w-72 lg-84 pt-3 mb-1">
<GlobalFilter filter={globalFilter} setFilter={setGlobalFilter} />
</div>
</div>
<div className="flex flex-col md:flex-row md:justify-between items-center mb-4">
<SearchInput />
<label>
<div>
Check All
<input
className="mx-2"
type="checkbox"
name="checkAll"
checked={userInfo === undefined ? dummyBool : table.getIsAllPageRowsSelected()}
onChange={
userInfo === undefined
? () => setDummyBool(!dummyBool)
: table.getToggleAllPageRowsSelectedHandler()
}
/>
<Button
className="rounded-[-0.25rem] rounded-r-[0.25rem]"
Comment on lines +114 to +115
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always thought it was weird that our button is not rounded on the left side. Let's keep the button normal and not straighten the left side, thanks.

variant="destructive"
onClick={userInfo === undefined ? () => void 0 : handleRemove}
>
Remove
</Button>
</div>
</label>
</div>
{validUid === 'FAILURE' && searchUid !== undefined ? (
<Alert variant="destructive" className="w-fit mb-7">
<AlertDescription>{searchUid} is not in any grouping.</AlertDescription>
</Alert>
) : (
''
)}
{validUid === undefined && searchUid !== '' ? (
<Alert variant="destructive" className="w-fit mb-7">
<AlertDescription>
There was an error searching for {searchUid}. <br />
Please ensure you have entered a valid UH member and try again.
</AlertDescription>
</Alert>
) : (
''
)}
{searchUid === '' ? (
<Alert variant="destructive" className="w-fit mb-7">
<AlertDescription>You must enter a UH member to search.</AlertDescription>
</Alert>
) : (
''
)}
Comment on lines +124 to +147
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same goes for here, use the && operator instead of the ternary.

<Table className="relative overflow-x-auto">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
onClick={
header.column.id === 'name'
? header.column.getToggleSortingHandler()
: () => void 0
}
Comment on lines +155 to +159
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to disable sorting for a certain column in your person-table-columns.tsx file. Then you can just use the getToggleSortingHandler() without this ternary,

className={`${header.column.id === 'name' ? 'w-1/4' : 'w-1/12'}`}
>
<div className="flex items-center">
{flexRender(header.column.columnDef.header, header.getContext())}
<SortArrow direction={header.column.getIsSorted()} />
</div>
</TableHead>
))}
</TableRow>
))}
</TableHeader>
{userInfo === undefined ? (
''
) : (
Comment on lines +171 to +173
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the && operator instead of the ternary, thanks.

<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} width={cell.column.columnDef.size}>
<div className="flex items-center px-2 overflow-hidden whitespace-nowrap">
<div className={`m-2 ${cell.column.id === 'name' ? 'w-full' : ''}`}>
{cell.column.id === 'name' && (
<div className="flex flex-row">
<PersonTableTooltip
value={'Manage grouping.'}
side={'top'}
desc={
<Link
href={`/groupings/${cell.row.original.path}`}
rel="noopener noreferrer"
target="_blank"
>
<ArrowUpRightFromSquare
size="1.25em"
className="text-text-primary"
data-testid={'arrow-up-right-from-square-icon'}
/>
</Link>
}
></PersonTableTooltip>
Comment on lines +183 to +199
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's preferred to pass in components as children instead of as a prop. Also the props value and side can be normal strings instead of strings wrapped {}.

<div className="pl-3">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
<PersonTableTooltip
value="Display the grouping's owners."
side={'right'}
desc={
<div className="ml-auto mr-3">
<CrownIcon
size="1.25em"
className="text-text-primary"
data-testid={'crown-icon'}
onClick={() =>
hydrateModal(cell.row.original.path)
}
/>
</div>
}
></PersonTableTooltip>
</div>
)}
</div>
{cell.column.id === 'inOwner' && (
<div className="ml-1">
<p className={`${cell.row.original.inOwner ? 'text-red-500' : ''}`}>
{cell.row.original.inOwner ? 'Yes' : 'No'}
</p>
</div>
)}
{cell.column.id === 'inBasisAndInclude' && (
<div>
<p
className={`${
cell.row.original.inBasisAndInclude ? 'text-red-500' : ''
}`}
>
{cell.row.original.inBasisAndInclude ? 'Yes' : 'No'}
</p>
</div>
)}
{cell.column.id === 'inInclude' && (
<div className="ml-2">
<p
className={`${
cell.row.original.inInclude ? 'text-red-500' : ''
}`}
>
{cell.row.original.inInclude ? 'Yes' : 'No'}
</p>
</div>
)}
{cell.column.id === 'inExclude' && (
<div className="ml-2">
<p
className={`${
cell.row.original.inExclude ? 'text-red-500' : ''
}`}
>
{cell.row.original.inExclude ? 'Yes' : 'No'}
</p>
</div>
)}
{cell.column.id === 'remove' && (
<div className="ml-3">
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
)}
Comment on lines +222 to +270
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These && statements should be moved to the person-table-columns.tsx file. Reference how the groupings-table-columns.tsx file does it.

</div>
</TableCell>
))}
</TableRow>
))}
</TableBody>
)}
</Table>
{userInfo === undefined ? '' : <PaginationBar table={table} />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the && operator here too.

</>
);
};

export default PersonTable;
Loading
Loading