From 70abe16340bccc2beaf6023d32559d3561a25e7f Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Mon, 20 Oct 2025 13:09:55 +0200 Subject: [PATCH 1/3] fix: data refreshing on first render is restricted to avoid multiple api calls and optimised the handling of search query params --- .../proposal/ProposalTableOfficer.tsx | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx b/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx index 7779c17dbc..0b27a519f2 100644 --- a/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx +++ b/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx @@ -380,13 +380,19 @@ const ProposalTableOfficer = ({ ); + const [isFirstRender, setIsFirstRender] = useState(true); + useEffect(() => { let isMounted = true; - if (isMounted) { + if (isMounted && !isFirstRender) { refreshTableData(); } + if (isFirstRender) { + setIsFirstRender(false); + } + return () => { isMounted = false; }; @@ -1120,10 +1126,24 @@ const ProposalTableOfficer = ({ return searchParams; }); }} + onRowsPerPageChange={(pageSize) => { + setSearchParams((searchParams) => { + searchParams.set('pageSize', pageSize.toString()); + searchParams.set('page', '0'); + + return searchParams; + }); + }} onSearchChange={(searchText) => { - setSearchParams({ - search: searchText ? searchText : '', - page: searchText ? '0' : page || '', + setSearchParams((searchParams) => { + if (searchText) { + searchParams.set('search', searchText); + searchParams.set('page', '0'); + } else { + searchParams.delete('search'); + } + + return searchParams; }); }} onSelectionChange={(selectedItems) => { @@ -1162,7 +1182,7 @@ const ProposalTableOfficer = ({ }, }), pageSize: pageSize ? +pageSize : undefined, - initialPage: search ? 0 : page ? +page : 0, + initialPage: page ? +page : 0, }} actions={tableActions} onChangeColumnHidden={(columnChange) => { @@ -1182,11 +1202,18 @@ const ProposalTableOfficer = ({ const [orderBy] = orderByCollection; if (!orderBy) { - setSearchParams({}); + setSearchParams((searchParams) => { + searchParams.delete('sortField'); + searchParams.delete('sortDirection'); + + return searchParams; + }); } else { - setSearchParams({ - sortField: orderBy?.orderByField, - sortDirection: orderBy?.orderDirection, + setSearchParams((searchParams) => { + searchParams.set('sortField', orderBy.orderByField); + searchParams.set('sortDirection', orderBy.orderDirection); + + return searchParams; }); } }} From 8d86eee82aa6a6b206574019bc2906096ffe5ea2 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Mon, 20 Oct 2025 13:38:02 +0200 Subject: [PATCH 2/3] fix: experiment table query params persisting --- .../experiment/ExperimentsTable.tsx | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/components/experiment/ExperimentsTable.tsx b/apps/frontend/src/components/experiment/ExperimentsTable.tsx index 53c3f35ff8..8973186742 100644 --- a/apps/frontend/src/components/experiment/ExperimentsTable.tsx +++ b/apps/frontend/src/components/experiment/ExperimentsTable.tsx @@ -10,6 +10,7 @@ import { useSearchParams } from 'react-router-dom'; import { Experiment, SettingsId } from 'generated/sdk'; import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; +import { setSortDirectionOnSortField } from 'utils/helperFunctions'; import useDataApiWithFeedback from 'utils/useDataApiWithFeedback'; import ExperimentReviewContent, { @@ -58,7 +59,7 @@ export default function ExperimentsTable({ const page = searchParams.get('page'); const pageSize = searchParams.get('pageSize'); const selectedExperimentId = searchParams.get('experiment'); - + const [isFirstRender, setIsFirstRender] = useState(true); const refreshTableData = () => { tableRef.current?.onQueryChange({}); }; @@ -75,10 +76,14 @@ export default function ExperimentsTable({ React.useEffect(() => { let isMounted = true; - if (isMounted) { + if (isMounted && !isFirstRender) { refreshTableData(); } + if (isFirstRender) { + setIsFirstRender(false); + } + return () => { isMounted = false; }; @@ -193,6 +198,12 @@ export default function ExperimentsTable({ ]; } + columns = setSortDirectionOnSortField( + columns, + searchParams.get('sortField'), + searchParams.get('sortDirection') + ); + const experimentReviewTabs = [ EXPERIMENT_MODAL_TAB_NAMES.EXPERIMENT_INFORMATION, EXPERIMENT_MODAL_TAB_NAMES.PROPOSAL_INFORMATION, @@ -215,12 +226,21 @@ export default function ExperimentsTable({ options={{ searchText: search || undefined, pageSize: pageSize ? +pageSize : 10, - initialPage: search ? 0 : page ? +page : 0, + initialPage: page ? +page : 0, + }} + onRowsPerPageChange={(pageSize) => { + setSearchParams((searchParams) => { + searchParams.set('pageSize', pageSize.toString()); + searchParams.set('page', '0'); + + return searchParams; + }); }} onSearchChange={(searchText) => { setSearchParams((searchParams) => { if (searchText) { searchParams.set('search', searchText); + searchParams.set('page', '0'); } else { searchParams.delete('search'); } @@ -235,6 +255,25 @@ export default function ExperimentsTable({ return searchParams; }); }} + onOrderCollectionChange={(orderByCollection) => { + const [orderBy] = orderByCollection; + + if (!orderBy) { + setSearchParams((searchParams) => { + searchParams.delete('sortField'); + searchParams.delete('sortDirection'); + + return searchParams; + }); + } else { + setSearchParams((searchParams) => { + searchParams.set('sortField', orderBy.orderByField); + searchParams.set('sortDirection', orderBy.orderDirection); + + return searchParams; + }); + } + }} /> {selectedExperiment && ( From e296a440ef3400f960a4ce1c4ee668139867a221 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Mon, 20 Oct 2025 14:46:18 +0200 Subject: [PATCH 3/3] People table enhancement and query persisting --- .../backend/src/datasources/UserDataSource.ts | 4 +- .../src/datasources/mockups/UserDataSource.ts | 8 ++- .../datasources/postgres/UserDataSource.ts | 69 ++++++++++++++----- apps/backend/src/queries/UserQueries.ts | 8 ++- .../src/resolvers/queries/UsersQuery.ts | 19 +++-- .../proposal/ParticipantSelector.tsx | 2 +- .../src/components/user/PeopleTable.tsx | 68 ++++++++++++++++-- .../src/graphql/user/getUsers.graphql | 12 ++-- 8 files changed, 150 insertions(+), 40 deletions(-) diff --git a/apps/backend/src/datasources/UserDataSource.ts b/apps/backend/src/datasources/UserDataSource.ts index f6c450a566..bd491c3bee 100644 --- a/apps/backend/src/datasources/UserDataSource.ts +++ b/apps/backend/src/datasources/UserDataSource.ts @@ -40,9 +40,11 @@ export interface UserDataSource { ): Promise<{ totalCount: number; users: BasicUserDetails[] }>; getPreviousCollaborators( user_id: number, - filter?: string, first?: number, offset?: number, + sortField?: string, + sortDirection?: string, + searchText?: string, userRole?: UserRole, subtractUsers?: [number] ): Promise<{ totalCount: number; users: BasicUserDetails[] }>; diff --git a/apps/backend/src/datasources/mockups/UserDataSource.ts b/apps/backend/src/datasources/mockups/UserDataSource.ts index 71cbcf9a07..a73b98c4ce 100644 --- a/apps/backend/src/datasources/mockups/UserDataSource.ts +++ b/apps/backend/src/datasources/mockups/UserDataSource.ts @@ -426,9 +426,13 @@ export class UserDataSourceMock implements UserDataSource { async getPreviousCollaborators( user_id: number, - filter?: string, first?: number, - offset?: number + offset?: number, + sortField?: string, + sortDirection?: string, + searchText?: string, + userRole?: UserRole, + subtractUsers?: [number] ): Promise<{ totalCount: number; users: BasicUserDetails[] }> { return { totalCount: 2, diff --git a/apps/backend/src/datasources/postgres/UserDataSource.ts b/apps/backend/src/datasources/postgres/UserDataSource.ts index 96395687bf..b4edc7f6db 100644 --- a/apps/backend/src/datasources/postgres/UserDataSource.ts +++ b/apps/backend/src/datasources/postgres/UserDataSource.ts @@ -33,6 +33,14 @@ import { createUserObject, } from './records'; +const fieldMap: { [key: string]: string } = { + created_at: 'created_at', + firstname: 'firstname', + preferredname: 'preferredname', + lastname: 'lastname', + institution: 'i.institution', +}; + export default class PostgresUserDataSource implements UserDataSource { async delete(id: number): Promise { return database('users') @@ -473,26 +481,25 @@ export default class PostgresUserDataSource implements UserDataSource { } async getUsers({ - filter, + searchText, first, offset, userRole, subtractUsers, - orderBy, - orderDirection = 'desc', + sortField = 'created_at', + sortDirection = 'desc', }: UsersArgs): Promise<{ totalCount: number; users: BasicUserDetails[] }> { return database .select(['*', database.raw('count(*) OVER() AS full_count')]) .from('users') .join('institutions as i', { 'users.institution_id': 'i.institution_id' }) - .orderBy('users.user_id', orderDirection) .modify((query) => { - if (filter) { + if (searchText) { query.andWhere((qb) => { - qb.whereILikeEscaped('institution', '%?%', filter) - .orWhereILikeEscaped('firstname', '%?%', filter) - .orWhereILikeEscaped('preferredname', '%?%', filter) - .orWhereILikeEscaped('lastname', '%?%', filter); + qb.whereILikeEscaped('institution', '%?%', searchText) + .orWhereILikeEscaped('firstname', '%?%', searchText) + .orWhereILikeEscaped('preferredname', '%?%', searchText) + .orWhereILikeEscaped('lastname', '%?%', searchText); }); } if (first) { @@ -509,8 +516,12 @@ export default class PostgresUserDataSource implements UserDataSource { if (subtractUsers && subtractUsers.length > 0) { query.whereNotIn('users.user_id', subtractUsers); } - if (orderBy) { - query.orderBy(orderBy, orderDirection); + if (sortField && sortDirection) { + if (!fieldMap.hasOwnProperty(sortField)) { + throw new GraphQLError(`Bad sort field given: ${sortField}`); + } + sortField = fieldMap[sortField]; + query.orderByRaw(`${sortField} ${sortDirection}`); } }) .then( @@ -529,14 +540,24 @@ export default class PostgresUserDataSource implements UserDataSource { async getPreviousCollaborators( userId: number, - filter?: string, first?: number, offset?: number, + sortField?: string, + sortDirection?: string, + searchText?: string, userRole?: UserRole, subtractUsers?: [number] ): Promise<{ totalCount: number; users: BasicUserDetails[] }> { if (userId == -1) { - return this.getUsers({ filter, first, offset, userRole, subtractUsers }); + return this.getUsers({ + searchText, + first, + offset, + userRole, + subtractUsers, + sortField, + sortDirection, + }); } const lastCollaborators = await this.getMostRecentCollaborators(userId); @@ -553,14 +574,26 @@ export default class PostgresUserDataSource implements UserDataSource { .join('institutions as i', { 'users.institution_id': 'i.institution_id' }) .whereIn('users.user_id', userIds) .modify((query) => { - if (filter) { + if (searchText) { query.andWhere((qb) => { - qb.whereILikeEscaped('institution', '%?%', filter) - .orWhereILikeEscaped('firstname', '%?%', filter) - .orWhereILikeEscaped('preferredname', '%?%', filter) - .orWhereILikeEscaped('lastname', '%?%', filter); + qb.whereILikeEscaped('institution', '%?%', searchText) + .orWhereILikeEscaped('firstname', '%?%', searchText) + .orWhereILikeEscaped('preferredname', '%?%', searchText) + .orWhereILikeEscaped('lastname', '%?%', searchText); }); } + logger.logInfo( + `Sort field: ${sortField}, direction: ${sortDirection}`, + {} + ); + if (sortField && sortDirection) { + if (!fieldMap.hasOwnProperty(sortField)) { + throw new GraphQLError(`Bad sort field given: ${sortField}`); + } + sortField = fieldMap[sortField]; + query.orderByRaw(`${sortField} ${sortDirection}`); + } + if (first) { query.limit(first); } diff --git a/apps/backend/src/queries/UserQueries.ts b/apps/backend/src/queries/UserQueries.ts index e6825399c6..19debbb077 100644 --- a/apps/backend/src/queries/UserQueries.ts +++ b/apps/backend/src/queries/UserQueries.ts @@ -138,17 +138,21 @@ export default class UserQueries { async getPreviousCollaborators( agent: UserWithRole | null, userId: number, - filter?: string, first?: number, offset?: number, + sortField?: string, + sortDirection?: string, + searchText?: string, userRole?: UserRole, subtractUsers?: [number] ) { return this.dataSource.getPreviousCollaborators( userId, - filter, first, offset, + sortField, + sortDirection, + searchText, userRole, subtractUsers ); diff --git a/apps/backend/src/resolvers/queries/UsersQuery.ts b/apps/backend/src/resolvers/queries/UsersQuery.ts index b1c0c84496..3cd0eef271 100644 --- a/apps/backend/src/resolvers/queries/UsersQuery.ts +++ b/apps/backend/src/resolvers/queries/UsersQuery.ts @@ -40,11 +40,14 @@ export class UsersArgs { @Field(() => [Int], { nullable: 'itemsAndList' }) subtractUsers?: [number]; - @Field(() => String, { nullable: true }) - orderBy?: string; + @Field({ nullable: true }) + public sortField?: string; - @Field(() => String, { nullable: true }) - orderDirection?: string; + @Field({ nullable: true }) + public sortDirection?: string; + + @Field({ nullable: true }) + public searchText?: string; } @ArgsType() @@ -65,20 +68,24 @@ export class UsersQuery { @Args() { userId, - filter, first, offset, userRole, subtractUsers, + sortField, + sortDirection, + searchText, }: PreviousCollaboratorsArgs, @Ctx() context: ResolverContext ) { return context.queries.user.getPreviousCollaborators( context.user, userId, - filter, first, offset, + sortField, + sortDirection, + searchText, userRole, subtractUsers ); diff --git a/apps/frontend/src/components/proposal/ParticipantSelector.tsx b/apps/frontend/src/components/proposal/ParticipantSelector.tsx index d19dbc0587..15dce7f63c 100644 --- a/apps/frontend/src/components/proposal/ParticipantSelector.tsx +++ b/apps/frontend/src/components/proposal/ParticipantSelector.tsx @@ -155,8 +155,8 @@ function ParticipantSelector({ ]; const { users } = await api().getUsers({ - filter: query, subtractUsers: excludedUserIds, + searchText: query, }); setOptions(users?.users || []); diff --git a/apps/frontend/src/components/user/PeopleTable.tsx b/apps/frontend/src/components/user/PeopleTable.tsx index 2e5379fc46..ae40d91b21 100644 --- a/apps/frontend/src/components/user/PeopleTable.tsx +++ b/apps/frontend/src/components/user/PeopleTable.tsx @@ -15,6 +15,7 @@ import { Formik } from 'formik'; import { TFunction } from 'i18next'; import React, { useState, useEffect, useContext } from 'react'; import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; import { ActionButtonContainer } from 'components/common/ActionButtonContainer'; import MaterialTable from 'components/common/DenseMaterialTable'; @@ -32,6 +33,7 @@ import { BasicUserDetailsFragment, } from 'generated/sdk'; import { useDataApi } from 'hooks/common/useDataApi'; +import { setSortDirectionOnSortField } from 'utils/helperFunctions'; import { tableIcons } from 'utils/materialIcons'; import { FunctionType } from 'utils/utilTypes'; @@ -204,6 +206,7 @@ const PeopleTable = ({ const [invitedUsers, setInvitedUsers] = useState([]); const [tableEmails, setTableEmails] = useState([]); const { t } = useTranslation(); + const [searchParams, setSearchParams] = useSearchParams(); const tableRef = React.createRef>(); @@ -361,13 +364,13 @@ const PeopleTable = ({ try { const [orderBy] = tableQuery.orderByCollection; const { users } = await api().getUsers({ - filter: tableQuery.search, first: tableQuery.pageSize, offset: tableQuery.page * tableQuery.pageSize, - orderBy: orderBy?.orderByField, - orderDirection: orderBy?.orderDirection, subtractUsers: query.subtractUsers, userRole: query.userRole, + sortField: orderBy?.orderByField, + sortDirection: orderBy?.orderDirection, + searchText: tableQuery.search, }); const filteredData = data @@ -515,16 +518,73 @@ const PeopleTable = ({ {title} } - columns={columns ?? localColumns} + columns={setSortDirectionOnSortField( + columns ? columns : localColumns, + searchParams.get('sortField'), + searchParams.get('sortDirection') + )} onSelectionChange={handleColumnSelectionChange} data={fetchRemoteUsersData} + onPageChange={(page) => { + setSearchParams((searchParams) => { + searchParams.set('page', page.toString()); + + return searchParams; + }); + }} + onRowsPerPageChange={(pageSize) => { + setSearchParams((searchParams) => { + searchParams.set('pageSize', pageSize.toString()); + searchParams.set('page', '0'); + + return searchParams; + }); + }} + onSearchChange={(searchText) => { + setSearchParams((searchParams) => { + if (searchText) { + searchParams.set('search', searchText); + searchParams.set('page', '0'); + } else { + searchParams.delete('search'); + } + + return searchParams; + }); + }} + onOrderCollectionChange={(orderByCollection) => { + const [orderBy] = orderByCollection; + + if (!orderBy) { + setSearchParams((searchParams) => { + searchParams.delete('sortField'); + searchParams.delete('sortDirection'); + + return searchParams; + }); + } else { + setSearchParams((searchParams) => { + searchParams.set('sortField', orderBy.orderByField); + searchParams.set('sortDirection', orderBy.orderDirection); + + return searchParams; + }); + } + }} options={{ search: search, + searchText: searchParams.get('search') || undefined, debounceInterval: 400, selection: selection, headerSelectionProps: { inputProps: { 'aria-label': 'Select All Rows' }, }, + pageSize: searchParams.get('pageSize') + ? +searchParams.get('pageSize')! + : undefined, + initialPage: searchParams.get('page') + ? +searchParams.get('page')! + : 0, ...mtOptions, selectionProps: (rowdata: BasicUserDetails) => ({ inputProps: { diff --git a/apps/frontend/src/graphql/user/getUsers.graphql b/apps/frontend/src/graphql/user/getUsers.graphql index ffed748e5f..696749a5dd 100644 --- a/apps/frontend/src/graphql/user/getUsers.graphql +++ b/apps/frontend/src/graphql/user/getUsers.graphql @@ -1,20 +1,20 @@ query getUsers( - $filter: String $first: Int $offset: Int $userRole: UserRole $subtractUsers: [Int!] - $orderBy: String - $orderDirection: String + $sortField: String + $sortDirection: String + $searchText: String ) { users( - filter: $filter first: $first offset: $offset userRole: $userRole subtractUsers: $subtractUsers - orderBy: $orderBy - orderDirection: $orderDirection + sortField: $sortField + sortDirection: $sortDirection + searchText: $searchText ) { users { ...basicUserDetails