From f267d843d86704cf510ed85dd86a439213f561fa Mon Sep 17 00:00:00 2001 From: adarshm11 Date: Mon, 24 Aug 2026 13:07:59 -0700 Subject: [PATCH 1/2] add custom pagination to user overview --- api/main_endpoints/routes/User.js | 31 ++++++++++++--- src/APIFunctions/User.js | 4 ++ src/Pages/Overview/Overview.js | 64 +++++++++++++++++++++++++++++-- 3 files changed, 90 insertions(+), 9 deletions(-) diff --git a/api/main_endpoints/routes/User.js b/api/main_endpoints/routes/User.js index 928908360..b0ba66da5 100644 --- a/api/main_endpoints/routes/User.js +++ b/api/main_endpoints/routes/User.js @@ -29,6 +29,8 @@ const {sendUnsubscribeEmail} = require('../util/emailHelpers'); const crypto = require('crypto'); const ROWS_PER_PAGE = 20; +const ALLOWED_ROWS_PER_PAGE = [10, 20, 50]; +const ALL_ROWS = 'all'; const SENSITIVE_FIELDS = [ 'email', @@ -220,15 +222,34 @@ router.post('/users', async function(req, res) { }; const sortOrder = orderToInteger[req.query.order] || orderToInteger.default; + const total = await User.count(maybeOr); + + // 'all' collapses every match onto a single page. anything we don't + // recognize falls back to the default rather than erroring, so callers that + // don't send a size keep working unchanged. + const requestedRowsPerPage = req.body.rowsPerPage; + const showAllRows = requestedRowsPerPage === ALL_ROWS; + let rowsPerPage = ROWS_PER_PAGE; + if (showAllRows) { + rowsPerPage = total; + } else if (ALLOWED_ROWS_PER_PAGE.includes(Number(requestedRowsPerPage))) { + rowsPerPage = Number(requestedRowsPerPage); + } + + // mongo reads a limit of 0 as "no limit", which is what we want for 'all' + // and is also why we can't just pass rowsPerPage straight through + const limit = showAllRows ? 0 : rowsPerPage; + // make sure that the page we want to see is 0 by default // and avoid negative page numbers - let skip = Math.max(Number(req.body.page) || 0, 0); - skip *= ROWS_PER_PAGE; - const total = await User.count(maybeOr); - User.find(maybeOr, { password: 0, }, { skip, limit: ROWS_PER_PAGE, }) + const skip = showAllRows + ? 0 + : Math.max(Number(req.body.page) || 0, 0) * rowsPerPage; + + User.find(maybeOr, { password: 0, }, { skip, limit }) .sort({ [sortColumn] : sortOrder }) .then(items => { - res.status(OK).send({ items, total, rowsPerPage: ROWS_PER_PAGE, }); + res.status(OK).send({ items, total, rowsPerPage, }); }) .catch((e) => { res.sendStatus(BAD_REQUEST); diff --git a/src/APIFunctions/User.js b/src/APIFunctions/User.js index 23ac761f3..59cc380b5 100644 --- a/src/APIFunctions/User.js +++ b/src/APIFunctions/User.js @@ -4,6 +4,8 @@ import { BASE_API_URL, membershipState, userFilterType } from '../Enums'; /** * Queries the database for all users. * @param {string} token The jwt token for verification + * @param {(number|'all'|null)} rowsPerPage How many users to return per page. + * Accepts 10, 20, 50, or 'all'; anything else falls back to the server default. * @returns {UserApiResponse} Containing any error information or the array of * users. */ @@ -14,6 +16,7 @@ export async function getAllUsers({ sortColumn = null, sortOrder = null, minRole = null, + rowsPerPage = null, }) { const url = new URL('/api/User/users', BASE_API_URL); @@ -37,6 +40,7 @@ export async function getAllUsers({ query, page, minRole, + rowsPerPage, }), }); if (res.ok) { diff --git a/src/Pages/Overview/Overview.js b/src/Pages/Overview/Overview.js index fd5ca50eb..2e023966c 100644 --- a/src/Pages/Overview/Overview.js +++ b/src/Pages/Overview/Overview.js @@ -10,6 +10,14 @@ import ConfirmationModal from const enums = require('../../Enums.js'); import { useSCE } from '../../Components/context/SceContext.js'; +const ALL_ROWS = 'all'; +const DEFAULT_PAGE_SIZE = 20; +const PAGE_SIZE_OPTIONS = [10, DEFAULT_PAGE_SIZE, 50, ALL_ROWS]; + +function pageSizeLabel(pageSize) { + return pageSize === ALL_ROWS ? 'All' : String(pageSize); +} + export default function Overview() { const { user } = useSCE(); const [toggleDelete, setToggleDelete] = useState(false); @@ -22,6 +30,10 @@ export default function Overview() { const [queryResult, setQueryResult] = useState([]); const [rowsPerPage, setRowsPerPage] = useState(0); const [query, setQuery] = useState(''); + // what the user picked in the dropdown. distinct from rowsPerPage, which is + // the size the server actually used -- under 'all' those two differ. + const [pageSizeChoice, setPageSizeChoice] = useState(DEFAULT_PAGE_SIZE); + const [isPageSizeDropdownOpen, setIsPageSizeDropdownOpen] = useState(false); const [currentSortColumn, setCurrentSortColumn] = useState('joinDate'); const [currentSortOrder, setCurrentSortOrder] = useState('desc'); const [clubRevenueData, setClubRevenueData] = useState({newMembersThisYear:0, newSingleSemesterMembers:0, newAnnualMembers:0, currentActiveMembers:0}); @@ -69,7 +81,8 @@ export default function Overview() { query: query, page: page, sortColumn: sortColumn, - sortOrder: sortOrder + sortOrder: sortOrder, + rowsPerPage: pageSizeChoice }); if (!apiResponse.error) { setUsers(apiResponse.responseData.items); @@ -89,7 +102,7 @@ export default function Overview() { useEffect(() => { callDatabase(); getClubRevenueData(); - }, [page, currentSortColumn, currentSortOrder]); + }, [page, currentSortColumn, currentSortOrder, pageSizeChoice]); useEffect(() => { @@ -127,6 +140,13 @@ export default function Overview() { } } + function handlePageSizeChange(pageSize) { + setPageSizeChoice(pageSize); + setIsPageSizeDropdownOpen(false); + // whatever page we were on probably doesn't exist at the new size + setPage(0); + } + function handleArrowVisibility(sortOrder, columnName) { if (currentSortOrder === sortOrder && currentSortColumn === columnName) return ''; @@ -252,8 +272,8 @@ export default function Overview() {

Current Active Members: {clubRevenueData.currentActiveMembers}

-
-