From 70abe16340bccc2beaf6023d32559d3561a25e7f Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Mon, 20 Oct 2025 13:09:55 +0200 Subject: [PATCH 1/9] 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/9] 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/9] 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 From 0b3d0656e307e5d4ff9ddc7af21bcd86169541d5 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Tue, 18 Nov 2025 23:45:19 +0100 Subject: [PATCH 4/9] Eliminating security issue by avoiding order by and validating the sort direction --- .../src/datasources/ExperimentDataSource.ts | 3 +- .../src/datasources/ProposalDataSource.ts | 5 +-- .../backend/src/datasources/UserDataSource.ts | 3 +- .../mockups/ExperimentDataSource.ts | 3 +- .../datasources/mockups/ProposalDataSource.ts | 5 +-- .../src/datasources/mockups/UserDataSource.ts | 3 +- .../postgres/ExperimentDataSource.ts | 5 +-- .../postgres/ProposalDataSource.ts | 7 ++-- .../postgres/StatusActionsLogsDataSource.ts | 4 +-- .../postgres/TemplateDataSource.ts | 2 +- .../datasources/postgres/UserDataSource.ts | 15 ++++---- .../stfc/StfcProposalDataSource.ts | 5 +-- .../datasources/stfc/StfcUserDataSource.ts | 5 +-- apps/backend/src/queries/ExperimentQueries.ts | 3 +- apps/backend/src/queries/ProposalQueries.ts | 5 +-- apps/backend/src/queries/UserQueries.ts | 3 +- .../src/resolvers/queries/ExperimentsQuery.ts | 5 +-- .../resolvers/queries/ProposalsViewQuery.ts | 5 +-- .../src/resolvers/queries/QuestionsQuery.ts | 5 +-- .../queries/StatusActionsLogsQuery.ts | 5 +-- .../src/resolvers/queries/UsersQuery.ts | 5 +-- apps/backend/src/resolvers/registerEnums.ts | 4 +++ apps/backend/src/utils/pagination.ts | 4 +++ .../components/common/SuperMaterialTable.tsx | 13 +++++-- .../experiment/ExperimentsTable.tsx | 35 +++++++++++-------- .../ProposalTableInstrumentScientist.tsx | 11 +++++- .../proposal/ProposalTableOfficer.tsx | 16 +++++++-- .../review/ProposalTableReviewer.tsx | 7 +++- .../StatusActionsLogsTable.tsx | 18 ++++++++-- .../TechniqueProposalTable.tsx | 18 ++++++++-- .../src/components/template/QuestionsPage.tsx | 15 ++++++-- .../src/components/user/PeopleTable.tsx | 15 ++++++-- .../experiment/getAllExperiments.graphql | 2 +- .../graphql/proposal/getProposalsCore.graphql | 2 +- .../getTechniqueScientistProposals.graphql | 2 +- .../getStatusActionsLogs.graphql | 18 ++++++++-- .../graphql/template/getAllQuestions.graphql | 6 ++-- .../src/graphql/user/getUsers.graphql | 2 +- .../src/hooks/experiment/useAllExperiments.ts | 4 +-- .../hooks/proposal/useProposalsCoreData.ts | 9 +++-- apps/frontend/src/utils/helperFunctions.tsx | 13 ++++--- 41 files changed, 224 insertions(+), 91 deletions(-) create mode 100644 apps/backend/src/utils/pagination.ts diff --git a/apps/backend/src/datasources/ExperimentDataSource.ts b/apps/backend/src/datasources/ExperimentDataSource.ts index a7ca421f84..4439102c22 100644 --- a/apps/backend/src/datasources/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/ExperimentDataSource.ts @@ -12,6 +12,7 @@ import { ExperimentsFilter, UserExperimentsFilter, } from '../resolvers/queries/ExperimentsQuery'; +import { PaginationSortDirection } from '../utils/pagination'; import { ExperimentSafetyEventsRecord } from './postgres/records'; export interface ExperimentDataSource { @@ -88,7 +89,7 @@ export interface ExperimentDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ totalCount: number; diff --git a/apps/backend/src/datasources/ProposalDataSource.ts b/apps/backend/src/datasources/ProposalDataSource.ts index 6cd43080cd..e8fc614857 100644 --- a/apps/backend/src/datasources/ProposalDataSource.ts +++ b/apps/backend/src/datasources/ProposalDataSource.ts @@ -6,6 +6,7 @@ import { TechnicalReview } from '../models/TechnicalReview'; import { UserWithRole } from '../models/User'; import { UpdateTechnicalReviewAssigneeInput } from '../resolvers/mutations/UpdateTechnicalReviewAssigneeMutation'; import { UserProposalsFilter } from '../resolvers/types/User'; +import { PaginationSortDirection } from '../utils/pagination'; import { ProposalsFilter } from './../resolvers/queries/ProposalsQuery'; import { ProposalEventsRecord } from './postgres/records'; @@ -15,7 +16,7 @@ export interface ProposalDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ totalCount: number; proposalViews: ProposalView[] }>; // Read @@ -89,7 +90,7 @@ export interface ProposalDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ totalCount: number; proposals: ProposalView[] }>; } diff --git a/apps/backend/src/datasources/UserDataSource.ts b/apps/backend/src/datasources/UserDataSource.ts index 2f34a26f93..66e6e0f034 100644 --- a/apps/backend/src/datasources/UserDataSource.ts +++ b/apps/backend/src/datasources/UserDataSource.ts @@ -9,6 +9,7 @@ import { UpdateUserByIdArgs, } from '../resolvers/mutations/UpdateUserMutation'; import { UsersArgs } from '../resolvers/queries/UsersQuery'; +import { PaginationSortDirection } from '../utils/pagination'; export interface UserDataSource { delete(id: number): Promise; @@ -43,7 +44,7 @@ export interface UserDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string, userRole?: UserRole, subtractUsers?: [number] diff --git a/apps/backend/src/datasources/mockups/ExperimentDataSource.ts b/apps/backend/src/datasources/mockups/ExperimentDataSource.ts index 67b9c50d54..70388e15c3 100644 --- a/apps/backend/src/datasources/mockups/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/mockups/ExperimentDataSource.ts @@ -14,6 +14,7 @@ import { UserExperimentsFilter, ExperimentsFilter, } from '../../resolvers/queries/ExperimentsQuery'; +import { PaginationSortDirection } from '../../utils/pagination'; import { ExperimentDataSource } from '../ExperimentDataSource'; import { ExperimentSafetyEventsRecord } from '../postgres/records'; @@ -122,7 +123,7 @@ export class ExperimentDataSourceMock implements ExperimentDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ totalCount: number; experiments: Experiment[] }> { return { diff --git a/apps/backend/src/datasources/mockups/ProposalDataSource.ts b/apps/backend/src/datasources/mockups/ProposalDataSource.ts index 6e496eb67a..6ab71378a6 100644 --- a/apps/backend/src/datasources/mockups/ProposalDataSource.ts +++ b/apps/backend/src/datasources/mockups/ProposalDataSource.ts @@ -10,6 +10,7 @@ import { } from '../../models/TechnicalReview'; import { UserWithRole } from '../../models/User'; import { UpdateTechnicalReviewAssigneeInput } from '../../resolvers/mutations/UpdateTechnicalReviewAssigneeMutation'; +import { PaginationSortDirection } from '../../utils/pagination'; import { ProposalEventsRecord } from '../postgres/records'; import { ProposalDataSource } from '../ProposalDataSource'; import { ProposalsFilter } from './../../resolvers/queries/ProposalsQuery'; @@ -137,7 +138,7 @@ export class ProposalDataSourceMock implements ProposalDataSource { first?: number | undefined, offset?: number | undefined, sortField?: string | undefined, - sortDirection?: string | undefined, + sortDirection?: PaginationSortDirection | undefined, searchText?: string | undefined ): Promise<{ totalCount: number; proposalViews: ProposalView[] }> { return { totalCount: 0, proposalViews: [] }; @@ -417,7 +418,7 @@ export class ProposalDataSourceMock implements ProposalDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ) { return { totalCount: 1, proposals: [dummyProposalView] }; diff --git a/apps/backend/src/datasources/mockups/UserDataSource.ts b/apps/backend/src/datasources/mockups/UserDataSource.ts index 730dc0f9d7..3ffa1436a8 100644 --- a/apps/backend/src/datasources/mockups/UserDataSource.ts +++ b/apps/backend/src/datasources/mockups/UserDataSource.ts @@ -14,6 +14,7 @@ import { UpdateUserByOidcSubArgs, } from '../../resolvers/mutations/UpdateUserMutation'; import { UsersArgs } from '../../resolvers/queries/UsersQuery'; +import { PaginationSortDirection } from '../../utils/pagination'; import { UserDataSource } from '../UserDataSource'; export const basicDummyUser = new BasicUserDetails( @@ -445,7 +446,7 @@ export class UserDataSourceMock implements UserDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string, userRole?: UserRole, subtractUsers?: [number] diff --git a/apps/backend/src/datasources/postgres/ExperimentDataSource.ts b/apps/backend/src/datasources/postgres/ExperimentDataSource.ts index 02b9a1cb9a..68cd99d066 100644 --- a/apps/backend/src/datasources/postgres/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/postgres/ExperimentDataSource.ts @@ -16,6 +16,7 @@ import { ExperimentsFilter, UserExperimentsFilter, } from '../../resolvers/queries/ExperimentsQuery'; +import { PaginationSortDirection } from '../../utils/pagination'; import { ExperimentDataSource } from '../ExperimentDataSource'; import database from './database'; import { @@ -571,7 +572,7 @@ export default class PostgresExperimentDataSource first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ totalCount: number; experiments: Experiment[] }> { //print all arguments @@ -673,7 +674,7 @@ export default class PostgresExperimentDataSource throw new GraphQLError(`Bad sort field given: ${sortField}`); } sortField = fieldMap[sortField]; - query.orderByRaw(`${sortField} ${sortDirection}`); + query.orderBy(sortField, sortDirection); } if (first) { diff --git a/apps/backend/src/datasources/postgres/ProposalDataSource.ts b/apps/backend/src/datasources/postgres/ProposalDataSource.ts index e1609b21f0..6e8f27f158 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -19,6 +19,7 @@ import { UserWithRole } from '../../models/User'; import { WorkflowConnectionWithStatus } from '../../models/WorkflowConnections'; import { UpdateTechnicalReviewAssigneeInput } from '../../resolvers/mutations/UpdateTechnicalReviewAssigneeMutation'; import { UserProposalsFilter } from '../../resolvers/types/User'; +import { PaginationSortDirection } from '../../utils/pagination'; import { AdminDataSource } from '../AdminDataSource'; import { ProposalDataSource } from '../ProposalDataSource'; import { WorkflowDataSource } from '../WorkflowDataSource'; @@ -382,7 +383,7 @@ export default class PostgresProposalDataSource implements ProposalDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string, principleInvestigator?: number[] ): Promise<{ totalCount: number; proposalViews: ProposalView[] }> { @@ -459,7 +460,7 @@ export default class PostgresProposalDataSource implements ProposalDataSource { throw new GraphQLError(`Bad sort field given: ${sortField}`); } sortField = fieldMap[sortField]; - query.orderByRaw(`${sortField} ${sortDirection}`); + query.orderBy(sortField, sortDirection); } if (filter?.referenceNumbers) { @@ -1067,7 +1068,7 @@ export default class PostgresProposalDataSource implements ProposalDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ totalCount: number; proposals: ProposalView[] }> { /* diff --git a/apps/backend/src/datasources/postgres/StatusActionsLogsDataSource.ts b/apps/backend/src/datasources/postgres/StatusActionsLogsDataSource.ts index 04c5a45651..64ad162d85 100644 --- a/apps/backend/src/datasources/postgres/StatusActionsLogsDataSource.ts +++ b/apps/backend/src/datasources/postgres/StatusActionsLogsDataSource.ts @@ -162,9 +162,7 @@ export default class PostgresStatusActionsLogsDataSource } if (args.sortField && args.sortDirection) { if (args.sortField in fieldMap) { - query.orderByRaw( - `${fieldMap[args.sortField]} ${args.sortDirection}` - ); + query.orderBy(fieldMap[args.sortField], args.sortDirection); } else { throw new GraphQLError(`Bad sort field given: ${args.sortField}`); } diff --git a/apps/backend/src/datasources/postgres/TemplateDataSource.ts b/apps/backend/src/datasources/postgres/TemplateDataSource.ts index f19fa6500e..185b53b8a4 100644 --- a/apps/backend/src/datasources/postgres/TemplateDataSource.ts +++ b/apps/backend/src/datasources/postgres/TemplateDataSource.ts @@ -191,7 +191,7 @@ export default class PostgresTemplateDataSource implements TemplateDataSource { throw new GraphQLError(`Bad sort field given: ${sortField}`); } - query.orderByRaw(`${this.fieldMap[sortField]} ${sortDirection}`); + query.orderBy(this.fieldMap[sortField], sortDirection); } if (first) { diff --git a/apps/backend/src/datasources/postgres/UserDataSource.ts b/apps/backend/src/datasources/postgres/UserDataSource.ts index 87000b1c37..bffc41d151 100644 --- a/apps/backend/src/datasources/postgres/UserDataSource.ts +++ b/apps/backend/src/datasources/postgres/UserDataSource.ts @@ -19,6 +19,7 @@ import { UpdateUserByOidcSubArgs, } from '../../resolvers/mutations/UpdateUserMutation'; import { UsersArgs } from '../../resolvers/queries/UsersQuery'; +import { PaginationSortDirection } from '../../utils/pagination'; import { UserDataSource } from '../UserDataSource'; import database, { isUniqueConstraintError } from './database'; import { @@ -508,7 +509,7 @@ export default class PostgresUserDataSource implements UserDataSource { userRole, subtractUsers, sortField = 'created_at', - sortDirection = 'desc', + sortDirection, }: UsersArgs): Promise<{ totalCount: number; users: BasicUserDetails[] }> { return database .select(['*', database.raw('count(*) OVER() AS full_count')]) @@ -542,7 +543,7 @@ export default class PostgresUserDataSource implements UserDataSource { throw new GraphQLError(`Bad sort field given: ${sortField}`); } sortField = fieldMap[sortField]; - query.orderByRaw(`${sortField} ${sortDirection}`); + query.orderBy(sortField, sortDirection); } }) .then( @@ -564,7 +565,7 @@ export default class PostgresUserDataSource implements UserDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string, userRole?: UserRole, subtractUsers?: [number] @@ -603,16 +604,12 @@ export default class PostgresUserDataSource implements UserDataSource { .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}`); + query.orderBy(sortField, sortDirection); } if (first) { @@ -705,7 +702,7 @@ export default class PostgresUserDataSource implements UserDataSource { .from('pu') .whereIn('pu.proposal_pk', proposals) .groupBy('pu.user_id') - .orderByRaw('count(pu.user_id) DESC') + .orderBy('pu.user_id', 'desc') .limit(10) .then((users: { user_id: number }[]) => users.map((uid) => uid.user_id)); } diff --git a/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts b/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts index 0303aa7b6e..c74d7bc3b0 100644 --- a/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts @@ -9,6 +9,7 @@ import { Roles } from '../../models/Role'; import { UserWithRole } from '../../models/User'; import { ProposalViewTechnicalReview } from '../../resolvers/types/ProposalView'; import { removeDuplicates } from '../../utils/helperFunctions'; +import { PaginationSortDirection } from '../../utils/pagination'; import { CallDataSource } from '../CallDataSource'; import PostgresAdminDataSource from '../postgres/AdminDataSource'; import database from '../postgres/database'; @@ -224,7 +225,7 @@ export default class StfcProposalDataSource extends PostgresProposalDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ totalCount: number; proposalViews: ProposalView[] }> { const stfcUserIds: number[] = searchText @@ -279,7 +280,7 @@ export default class StfcProposalDataSource extends PostgresProposalDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ totalCount: number; proposals: ProposalView[] }> { return postgresProposalDataSource.getTechniqueScientistProposals( diff --git a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts index 70b06ccb5f..2a0e585b6e 100644 --- a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts @@ -15,6 +15,7 @@ import { } from '../../resolvers/mutations/UpdateUserMutation'; import { UsersArgs } from '../../resolvers/queries/UsersQuery'; import { Cache } from '../../utils/Cache'; +import { PaginationSortDirection } from '../../utils/pagination'; import PostgresUserDataSource from '../postgres/UserDataSource'; import { UserDataSource } from '../UserDataSource'; import { createUOWSClient } from './UOWSClient'; @@ -588,7 +589,7 @@ export class StfcUserDataSource implements UserDataSource { offset: offset, userRole: undefined, subtractUsers: subtractUsers, - sortDirection: 'asc', + sortDirection: PaginationSortDirection.asc, }); if (users[0]) { @@ -613,7 +614,7 @@ export class StfcUserDataSource implements UserDataSource { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string, userRole?: UserRole, subtractUsers?: [number] diff --git a/apps/backend/src/queries/ExperimentQueries.ts b/apps/backend/src/queries/ExperimentQueries.ts index c4b7269add..1953451bbf 100644 --- a/apps/backend/src/queries/ExperimentQueries.ts +++ b/apps/backend/src/queries/ExperimentQueries.ts @@ -11,6 +11,7 @@ import { ExperimentsFilter } from '../resolvers/queries/ExperimentsQuery'; import { Experiment } from '../resolvers/types/Experiment'; import { ExperimentHasSample } from '../resolvers/types/ExperimentHasSample'; import { ExperimentSafety } from '../resolvers/types/ExperimentSafety'; +import { PaginationSortDirection } from '../utils/pagination'; @injectable() export default class ExperimentQueries { @@ -97,7 +98,7 @@ export default class ExperimentQueries { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ) { let instrumentScientistUserId: number | undefined; diff --git a/apps/backend/src/queries/ProposalQueries.ts b/apps/backend/src/queries/ProposalQueries.ts index 909d2797b4..29334cd0eb 100644 --- a/apps/backend/src/queries/ProposalQueries.ts +++ b/apps/backend/src/queries/ProposalQueries.ts @@ -15,6 +15,7 @@ import { Roles } from '../models/Role'; import { UserWithRole } from '../models/User'; import { ProposalsFilter } from '../resolvers/queries/ProposalsQuery'; import { omit } from '../utils/helperFunctions'; +import { PaginationSortDirection } from '../utils/pagination'; @injectable() export default class ProposalQueries { @@ -96,7 +97,7 @@ export default class ProposalQueries { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ) { try { @@ -140,7 +141,7 @@ export default class ProposalQueries { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string ) { return this.dataSource.getTechniqueScientistProposals( diff --git a/apps/backend/src/queries/UserQueries.ts b/apps/backend/src/queries/UserQueries.ts index 8c55fa7c76..17baeafa94 100644 --- a/apps/backend/src/queries/UserQueries.ts +++ b/apps/backend/src/queries/UserQueries.ts @@ -16,6 +16,7 @@ import { } from '../models/User'; import { UsersArgs } from '../resolvers/queries/UsersQuery'; import { verifyToken } from '../utils/jwt'; +import { PaginationSortDirection } from '../utils/pagination'; @injectable() export default class UserQueries { @@ -143,7 +144,7 @@ export default class UserQueries { first?: number, offset?: number, sortField?: string, - sortDirection?: string, + sortDirection?: PaginationSortDirection, searchText?: string, userRole?: UserRole, subtractUsers?: [number] diff --git a/apps/backend/src/resolvers/queries/ExperimentsQuery.ts b/apps/backend/src/resolvers/queries/ExperimentsQuery.ts index c0b4e4cffe..71b41e673d 100644 --- a/apps/backend/src/resolvers/queries/ExperimentsQuery.ts +++ b/apps/backend/src/resolvers/queries/ExperimentsQuery.ts @@ -12,6 +12,7 @@ import { import { ResolverContext } from '../../context'; import { ExperimentStatus } from '../../models/Experiment'; +import { PaginationSortDirection } from '../../utils/pagination'; import { Experiment } from '../types/Experiment'; @InputType() @@ -61,8 +62,8 @@ export class ExperimentsArgs { @Field({ nullable: true }) public sortField?: string; - @Field({ nullable: true }) - public sortDirection?: string; + @Field(() => PaginationSortDirection, { nullable: true }) + public sortDirection?: PaginationSortDirection; @Field({ nullable: true }) public searchText?: string; diff --git a/apps/backend/src/resolvers/queries/ProposalsViewQuery.ts b/apps/backend/src/resolvers/queries/ProposalsViewQuery.ts index 4fe82a7779..d9b9ecac7c 100644 --- a/apps/backend/src/resolvers/queries/ProposalsViewQuery.ts +++ b/apps/backend/src/resolvers/queries/ProposalsViewQuery.ts @@ -11,6 +11,7 @@ import { } from 'type-graphql'; import { ResolverContext } from '../../context'; +import { PaginationSortDirection } from '../../utils/pagination'; import { ProposalScientistComment, ProposalView } from '../types/ProposalView'; import { ProposalsFilter } from './ProposalsQuery'; @@ -28,8 +29,8 @@ class ProposalsViewArgs { @Field({ nullable: true }) public sortField?: string; - @Field({ nullable: true }) - public sortDirection?: string; + @Field(() => PaginationSortDirection, { nullable: true }) + public sortDirection?: PaginationSortDirection; @Field({ nullable: true }) public searchText?: string; diff --git a/apps/backend/src/resolvers/queries/QuestionsQuery.ts b/apps/backend/src/resolvers/queries/QuestionsQuery.ts index 7b9662669e..a25d69cccd 100644 --- a/apps/backend/src/resolvers/queries/QuestionsQuery.ts +++ b/apps/backend/src/resolvers/queries/QuestionsQuery.ts @@ -13,6 +13,7 @@ import { import { ResolverContext } from '../../context'; import { DataType, TemplateCategoryId } from '../../models/Template'; +import { PaginationSortDirection } from '../../utils/pagination'; import { QuestionWithUsage } from '../types/QuestionWithUsage'; @InputType() @@ -68,8 +69,8 @@ export class AllQuestionsFilterArgs { @Field({ nullable: true }) public sortField?: string; - @Field({ nullable: true }) - public sortDirection?: string; + @Field(() => PaginationSortDirection, { nullable: true }) + public sortDirection?: PaginationSortDirection; @Field({ nullable: true }) public searchText?: string; diff --git a/apps/backend/src/resolvers/queries/StatusActionsLogsQuery.ts b/apps/backend/src/resolvers/queries/StatusActionsLogsQuery.ts index e5220526d7..bbd7d9da4c 100644 --- a/apps/backend/src/resolvers/queries/StatusActionsLogsQuery.ts +++ b/apps/backend/src/resolvers/queries/StatusActionsLogsQuery.ts @@ -12,6 +12,7 @@ import { import { ResolverContext } from '../../context'; import { StatusActionType } from '../../models/StatusAction'; +import { PaginationSortDirection } from '../../utils/pagination'; import { EmailStatusActionRecipients } from '../types/StatusActionConfig'; import { StatusActionsLog } from '../types/StatusActionsLog'; @@ -87,8 +88,8 @@ export class StatusActionsLogsFilterArgs { @Field({ nullable: true }) public sortField?: string; - @Field({ nullable: true }) - public sortDirection?: string; + @Field(() => PaginationSortDirection, { nullable: true }) + public sortDirection?: PaginationSortDirection; @Field({ nullable: true }) public searchText?: string; diff --git a/apps/backend/src/resolvers/queries/UsersQuery.ts b/apps/backend/src/resolvers/queries/UsersQuery.ts index 3cd0eef271..57ac0bd010 100644 --- a/apps/backend/src/resolvers/queries/UsersQuery.ts +++ b/apps/backend/src/resolvers/queries/UsersQuery.ts @@ -12,6 +12,7 @@ import { import { ResolverContext } from '../../context'; import { UserRole } from '../../models/User'; +import { PaginationSortDirection } from '../../utils/pagination'; import { BasicUserDetails } from '../types/BasicUserDetails'; @ObjectType() @@ -43,8 +44,8 @@ export class UsersArgs { @Field({ nullable: true }) public sortField?: string; - @Field({ nullable: true }) - public sortDirection?: string; + @Field(() => PaginationSortDirection, { nullable: true }) + public sortDirection?: PaginationSortDirection; @Field({ nullable: true }) public searchText?: string; diff --git a/apps/backend/src/resolvers/registerEnums.ts b/apps/backend/src/resolvers/registerEnums.ts index ee209990cf..954f6a3440 100644 --- a/apps/backend/src/resolvers/registerEnums.ts +++ b/apps/backend/src/resolvers/registerEnums.ts @@ -32,6 +32,7 @@ import { import { UserRole } from '../models/User'; import { VisitRegistrationStatus } from '../models/VisitRegistration'; import { WorkflowType } from '../models/Workflow'; +import { PaginationSortDirection } from '../utils/pagination'; import { QueryMutationAndServicesGroups } from './queries/GetAllQueryMutationAndServicesMethodsQuery'; import { NumberValueConstraint } from './types/FieldConfig'; import { EmailStatusActionRecipients } from './types/StatusActionConfig'; @@ -99,4 +100,7 @@ export const registerEnums = () => { registerEnumType(InstrumentScientistDecisionEnum, { name: 'InstrumentScientistDecisionEnum', }); + registerEnumType(PaginationSortDirection, { + name: 'PaginationSortDirection', + }); }; diff --git a/apps/backend/src/utils/pagination.ts b/apps/backend/src/utils/pagination.ts new file mode 100644 index 0000000000..0391550370 --- /dev/null +++ b/apps/backend/src/utils/pagination.ts @@ -0,0 +1,4 @@ +export enum PaginationSortDirection { + asc = 'ASC', + desc = 'DESC', +} diff --git a/apps/frontend/src/components/common/SuperMaterialTable.tsx b/apps/frontend/src/components/common/SuperMaterialTable.tsx index a1a2298a57..97b9329060 100644 --- a/apps/frontend/src/components/common/SuperMaterialTable.tsx +++ b/apps/frontend/src/components/common/SuperMaterialTable.tsx @@ -7,14 +7,13 @@ import { useSearchParams } from 'react-router-dom'; import { ActionButtonContainer } from 'components/common/ActionButtonContainer'; import MaterialTable from 'components/common/DenseMaterialTable'; +import { PaginationSortDirection } from 'generated/sdk'; import { setSortDirectionOnSortField } from 'utils/helperFunctions'; import { tableIcons } from 'utils/materialIcons'; import { FunctionType } from 'utils/utilTypes'; import StyledDialog from './StyledDialog'; -export type SortDirectionType = 'asc' | 'desc' | undefined; - interface SuperProps> { createModal?: ( onUpdate: ( @@ -90,7 +89,15 @@ export function SuperMaterialTable({ options.searchText = search || undefined; } - columns = setSortDirectionOnSortField(columns, sortField, sortDirection); + columns = setSortDirectionOnSortField( + columns, + sortField, + sortDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined + ); const onCreated = ( objectAdded: Entry | null, diff --git a/apps/frontend/src/components/experiment/ExperimentsTable.tsx b/apps/frontend/src/components/experiment/ExperimentsTable.tsx index 8973186742..a909649c8a 100644 --- a/apps/frontend/src/components/experiment/ExperimentsTable.tsx +++ b/apps/frontend/src/components/experiment/ExperimentsTable.tsx @@ -5,10 +5,10 @@ import MaterialTable, { } from '@material-table/core'; import { Visibility } from '@mui/icons-material'; import { IconButton, Tooltip, Typography } from '@mui/material'; -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { Experiment, SettingsId } from 'generated/sdk'; +import { Experiment, PaginationSortDirection, SettingsId } from 'generated/sdk'; import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; import { setSortDirectionOnSortField } from 'utils/helperFunctions'; import useDataApiWithFeedback from 'utils/useDataApiWithFeedback'; @@ -59,11 +59,12 @@ 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({}); }; + const isFirstRender = useRef(true); + React.useEffect(() => { setSelectedExperiment( tableData.find( @@ -74,19 +75,13 @@ export default function ExperimentsTable({ }, [selectedExperimentId, tableData]); React.useEffect(() => { - let isMounted = true; - - if (isMounted && !isFirstRender) { - refreshTableData(); - } + if (isFirstRender.current) { + isFirstRender.current = false; - if (isFirstRender) { - setIsFirstRender(false); + return; } + refreshTableData(); - return () => { - isMounted = false; - }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(experimentsFilter)]); @@ -116,7 +111,12 @@ export default function ExperimentsTable({ ...(experimentEndDate ? { experimentEndDate } : {}), }, sortField: orderBy?.orderByField, - sortDirection: orderBy?.orderDirection, + sortDirection: + orderBy?.orderDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : orderBy?.orderDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined, first: tableQuery.pageSize, offset: tableQuery.page * tableQuery.pageSize, searchText: tableQuery.search, @@ -197,11 +197,16 @@ export default function ExperimentsTable({ ...columns, ]; } + const sortDirection = searchParams.get('sortDirection'); columns = setSortDirectionOnSortField( columns, searchParams.get('sortField'), - searchParams.get('sortDirection') + sortDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined ); const experimentReviewTabs = [ diff --git a/apps/frontend/src/components/proposal/ProposalTableInstrumentScientist.tsx b/apps/frontend/src/components/proposal/ProposalTableInstrumentScientist.tsx index 9fc94c4263..76ecb10684 100644 --- a/apps/frontend/src/components/proposal/ProposalTableInstrumentScientist.tsx +++ b/apps/frontend/src/components/proposal/ProposalTableInstrumentScientist.tsx @@ -42,6 +42,7 @@ import { UserRole, ProposalViewTechnicalReviewAssignee, WorkflowType, + PaginationSortDirection, } from 'generated/sdk'; import { useInstrumentScientistCallsData } from 'hooks/call/useInstrumentScientistCallsData'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; @@ -758,7 +759,15 @@ const ProposalTableInstrumentScientist = ({ } else { removeColumns(columns, fapReviewColumns(t)); } - columns = setSortDirectionOnSortField(columns, sortField, sortDirection); + columns = setSortDirectionOnSortField( + columns, + sortField, + sortDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined + ); const GetAppIconComponent = (): JSX.Element => ( diff --git a/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx b/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx index bb602281f8..39b8331910 100644 --- a/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx +++ b/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx @@ -52,6 +52,7 @@ import { FapInstrumentInput, FeatureId, InstrumentMinimalFragment, + PaginationSortDirection, ProposalViewInstrument, ProposalsFilter, Status, @@ -626,10 +627,16 @@ const ProposalTableOfficer = ({ setBulkReassignData(currentUserAssignedSelectTechReviews); }; + const sortDirection = searchParams.get('sortDirection'); + columns = setSortDirectionOnSortField( columns, searchParams.get('sortField'), - searchParams.get('sortDirection') + sortDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined ); const reviewModal = searchParams.get('reviewModal'); @@ -679,7 +686,12 @@ const ProposalTableOfficer = ({ text: text, }, sortField: orderBy?.orderByField, - sortDirection: orderBy?.orderDirection, + sortDirection: + orderBy?.orderDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : orderBy?.orderDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined, first: tableQuery.pageSize, offset: tableQuery.page * tableQuery.pageSize, searchText: tableQuery.search, diff --git a/apps/frontend/src/components/review/ProposalTableReviewer.tsx b/apps/frontend/src/components/review/ProposalTableReviewer.tsx index 3e96bfd751..699cd6b108 100644 --- a/apps/frontend/src/components/review/ProposalTableReviewer.tsx +++ b/apps/frontend/src/components/review/ProposalTableReviewer.tsx @@ -17,6 +17,7 @@ import CallFilter from 'components/common/proposalFilters/CallFilter'; import InstrumentFilter from 'components/common/proposalFilters/InstrumentFilter'; import { UserContext } from 'context/UserContextProvider'; import { + PaginationSortDirection, ReviewerFilter, ReviewStatus, UserRole, @@ -176,7 +177,11 @@ const ProposalTableReviewer = ({ confirm }: { confirm: WithConfirmType }) => { const sortedColumns = setSortDirectionOnSortField( columns(t), sortField, - sortDirection + sortDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined ); /** diff --git a/apps/frontend/src/components/statusActionsLogs/StatusActionsLogsTable.tsx b/apps/frontend/src/components/statusActionsLogs/StatusActionsLogsTable.tsx index 4d72a3a876..dbffe78e64 100644 --- a/apps/frontend/src/components/statusActionsLogs/StatusActionsLogsTable.tsx +++ b/apps/frontend/src/components/statusActionsLogs/StatusActionsLogsTable.tsx @@ -13,6 +13,7 @@ import { Link as ReactRouterLink, useSearchParams } from 'react-router-dom'; import MaterialTable from 'components/common/DenseMaterialTable'; import CallFilter from 'components/common/proposalFilters/CallFilter'; import { + PaginationSortDirection, StatusActionsLog, StatusActionsLogsFilter, StatusActionType, @@ -115,7 +116,15 @@ const StatusActionsLogsTable = ({ return v; }); if (sortField && sortDirection) { - columns = setSortDirectionOnSortField(columns, sortField, sortDirection); + columns = setSortDirectionOnSortField( + columns, + sortField, + sortDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined + ); } if (localStorageValue) { columns = columns.map((column) => ({ @@ -176,7 +185,12 @@ const StatusActionsLogsTable = ({ filter, searchText: tableQuery.search, sortField: orderBy?.orderByField, - sortDirection: orderBy?.orderDirection, + sortDirection: + orderBy?.orderDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : orderBy?.orderDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined, first: tableQuery.pageSize, offset: tableQuery.page * tableQuery.pageSize, }); diff --git a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx index c35f57226c..3b5a66b2d1 100644 --- a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx +++ b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx @@ -30,6 +30,7 @@ import ProposalReviewContent, { import ProposalReviewModal from 'components/review/ProposalReviewModal'; import { UserContext } from 'context/UserContextProvider'; import { + PaginationSortDirection, ProposalsFilter, SettingsId, UserRole, @@ -618,7 +619,15 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { addColumns(columns, techniquesColumns()); addColumns(columns, statusColumn()); - columns = setSortDirectionOnSortField(columns, sortField, sortDirection); + columns = setSortDirectionOnSortField( + columns, + sortField, + sortDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined + ); const fetchRemoteProposalsData = (tableQuery: Query) => new Promise>(async (resolve, reject) => { @@ -653,7 +662,12 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { currentRole === UserRole.INSTRUMENT_SCIENTIST ? [9] : [], // Hide expired from scientists }, sortField: orderBy?.orderByField, - sortDirection: orderBy?.orderDirection, + sortDirection: + orderBy?.orderDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : orderBy?.orderDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined, first: tableQuery.pageSize, offset: tableQuery.page * tableQuery.pageSize, searchText: tableQuery.search, diff --git a/apps/frontend/src/components/template/QuestionsPage.tsx b/apps/frontend/src/components/template/QuestionsPage.tsx index ffc3186dad..e761f096b9 100644 --- a/apps/frontend/src/components/template/QuestionsPage.tsx +++ b/apps/frontend/src/components/template/QuestionsPage.tsx @@ -17,6 +17,7 @@ import { import { BasicUserDetailsFragment, DataType, + PaginationSortDirection, QuestionsFilter, TemplateCategoryId, } from 'generated/sdk'; @@ -98,7 +99,12 @@ function QuestionsPage() { filter, searchText: tableQuery.search, sortField: orderBy?.orderByField, - sortDirection: orderBy?.orderDirection, + sortDirection: + orderBy?.orderDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : orderBy?.orderDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined, first: tableQuery.pageSize, offset: tableQuery.page * tableQuery.pageSize, }); @@ -113,10 +119,15 @@ function QuestionsPage() { } }); + const sortDirection = searchParams.get('sortDirection'); columns = setSortDirectionOnSortField( columns, searchParams.get('sortField'), - searchParams.get('sortDirection') + sortDirection === PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection === PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined ); return ( diff --git a/apps/frontend/src/components/user/PeopleTable.tsx b/apps/frontend/src/components/user/PeopleTable.tsx index ae40d91b21..f8d59209d6 100644 --- a/apps/frontend/src/components/user/PeopleTable.tsx +++ b/apps/frontend/src/components/user/PeopleTable.tsx @@ -31,6 +31,7 @@ import { Maybe, getSdk, BasicUserDetailsFragment, + PaginationSortDirection, } from 'generated/sdk'; import { useDataApi } from 'hooks/common/useDataApi'; import { setSortDirectionOnSortField } from 'utils/helperFunctions'; @@ -210,6 +211,7 @@ const PeopleTable = ({ const tableRef = React.createRef>(); + const sortDirection = searchParams.get('sortDirection'); useEffect(() => { if (!data) { return; @@ -369,7 +371,12 @@ const PeopleTable = ({ subtractUsers: query.subtractUsers, userRole: query.userRole, sortField: orderBy?.orderByField, - sortDirection: orderBy?.orderDirection, + sortDirection: + orderBy?.orderDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : orderBy?.orderDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined, searchText: tableQuery.search, }); @@ -521,7 +528,11 @@ const PeopleTable = ({ columns={setSortDirectionOnSortField( columns ? columns : localColumns, searchParams.get('sortField'), - searchParams.get('sortDirection') + sortDirection === PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection === PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined )} onSelectionChange={handleColumnSelectionChange} data={fetchRemoteUsersData} diff --git a/apps/frontend/src/graphql/experiment/getAllExperiments.graphql b/apps/frontend/src/graphql/experiment/getAllExperiments.graphql index a236f88015..8b0a14c634 100644 --- a/apps/frontend/src/graphql/experiment/getAllExperiments.graphql +++ b/apps/frontend/src/graphql/experiment/getAllExperiments.graphql @@ -3,7 +3,7 @@ query getExperiments( $first: Int $offset: Int $sortField: String - $sortDirection: String + $sortDirection: PaginationSortDirection $searchText: String ) { allExperiments( diff --git a/apps/frontend/src/graphql/proposal/getProposalsCore.graphql b/apps/frontend/src/graphql/proposal/getProposalsCore.graphql index 08055a4f84..45bcfba83b 100644 --- a/apps/frontend/src/graphql/proposal/getProposalsCore.graphql +++ b/apps/frontend/src/graphql/proposal/getProposalsCore.graphql @@ -3,7 +3,7 @@ query getProposalsCore( $first: Int $offset: Int $sortField: String - $sortDirection: String + $sortDirection: PaginationSortDirection $searchText: String ) { proposalsView( diff --git a/apps/frontend/src/graphql/proposal/getTechniqueScientistProposals.graphql b/apps/frontend/src/graphql/proposal/getTechniqueScientistProposals.graphql index 4f86ebfd77..f4f63fa17b 100644 --- a/apps/frontend/src/graphql/proposal/getTechniqueScientistProposals.graphql +++ b/apps/frontend/src/graphql/proposal/getTechniqueScientistProposals.graphql @@ -3,7 +3,7 @@ query getTechniqueScientistProposals( $first: Int $offset: Int $sortField: String - $sortDirection: String + $sortDirection: PaginationSortDirection $searchText: String ) { techniqueScientistProposals( diff --git a/apps/frontend/src/graphql/statusActionsLogs/getStatusActionsLogs.graphql b/apps/frontend/src/graphql/statusActionsLogs/getStatusActionsLogs.graphql index a01de4f8f2..2151cfc1b5 100644 --- a/apps/frontend/src/graphql/statusActionsLogs/getStatusActionsLogs.graphql +++ b/apps/frontend/src/graphql/statusActionsLogs/getStatusActionsLogs.graphql @@ -1,5 +1,19 @@ -query getStatusActionsLogs($filter: StatusActionsLogsFilter,$searchText: String, $first: Int, $sortField: String, $sortDirection: String, $offset: Int) { - statusActionsLogs(filter: $filter, first: $first, sortField: $sortField, sortDirection: $sortDirection, searchText: $searchText, offset: $offset) { +query getStatusActionsLogs( + $filter: StatusActionsLogsFilter + $searchText: String + $first: Int + $sortField: String + $sortDirection: PaginationSortDirection + $offset: Int +) { + statusActionsLogs( + filter: $filter + first: $first + sortField: $sortField + sortDirection: $sortDirection + searchText: $searchText + offset: $offset + ) { statusActionsLogs { ...statusActionsLog } diff --git a/apps/frontend/src/graphql/template/getAllQuestions.graphql b/apps/frontend/src/graphql/template/getAllQuestions.graphql index 2ad5c589c6..e08a4e2929 100644 --- a/apps/frontend/src/graphql/template/getAllQuestions.graphql +++ b/apps/frontend/src/graphql/template/getAllQuestions.graphql @@ -1,9 +1,9 @@ query getAllQuestions( - $filter: AllQuestionsFilter + $filter: AllQuestionsFilter $first: Int $offset: Int $sortField: String - $sortDirection: String + $sortDirection: PaginationSortDirection $searchText: String ) { allQuestions( @@ -14,7 +14,7 @@ query getAllQuestions( sortDirection: $sortDirection searchText: $searchText ) { - questions{ + questions { id question naturalKey diff --git a/apps/frontend/src/graphql/user/getUsers.graphql b/apps/frontend/src/graphql/user/getUsers.graphql index 696749a5dd..298a73ab09 100644 --- a/apps/frontend/src/graphql/user/getUsers.graphql +++ b/apps/frontend/src/graphql/user/getUsers.graphql @@ -4,7 +4,7 @@ query getUsers( $userRole: UserRole $subtractUsers: [Int!] $sortField: String - $sortDirection: String + $sortDirection: PaginationSortDirection $searchText: String ) { users( diff --git a/apps/frontend/src/hooks/experiment/useAllExperiments.ts b/apps/frontend/src/hooks/experiment/useAllExperiments.ts index 5d97e2bc43..ed77621df0 100644 --- a/apps/frontend/src/hooks/experiment/useAllExperiments.ts +++ b/apps/frontend/src/hooks/experiment/useAllExperiments.ts @@ -1,13 +1,13 @@ import { useCallback, useEffect, useState } from 'react'; -import { ExperimentsFilter } from 'generated/sdk'; +import { ExperimentsFilter, PaginationSortDirection } from 'generated/sdk'; import { useDataApi } from 'hooks/common/useDataApi'; type QueryParameters = { first?: number; offset?: number; sortField?: string | undefined; - sortDirection?: string | undefined; + sortDirection?: PaginationSortDirection | undefined; searchText?: string | undefined; refetch?: boolean; }; diff --git a/apps/frontend/src/hooks/proposal/useProposalsCoreData.ts b/apps/frontend/src/hooks/proposal/useProposalsCoreData.ts index 75b48525cf..7bd875eb8b 100644 --- a/apps/frontend/src/hooks/proposal/useProposalsCoreData.ts +++ b/apps/frontend/src/hooks/proposal/useProposalsCoreData.ts @@ -5,14 +5,19 @@ import { import { useCallback, useContext, useEffect, useState } from 'react'; import { UserContext } from 'context/UserContextProvider'; -import { ProposalsFilter, ProposalView, UserRole } from 'generated/sdk'; +import { + PaginationSortDirection, + ProposalsFilter, + ProposalView, + UserRole, +} from 'generated/sdk'; import { useDataApi } from 'hooks/common/useDataApi'; type QueryParameters = { first?: number; offset?: number; sortField?: string | undefined; - sortDirection?: string | undefined; + sortDirection?: PaginationSortDirection | undefined; searchText?: string | undefined; refetch?: boolean; }; diff --git a/apps/frontend/src/utils/helperFunctions.tsx b/apps/frontend/src/utils/helperFunctions.tsx index ac79c74bee..927b01579b 100644 --- a/apps/frontend/src/utils/helperFunctions.tsx +++ b/apps/frontend/src/utils/helperFunctions.tsx @@ -7,8 +7,13 @@ import { import React from 'react'; import * as Yup from 'yup'; -import { SortDirectionType } from 'components/common/SuperMaterialTable'; -import { Proposal, ProposalEndStatus, Scalars, Status } from 'generated/sdk'; +import { + PaginationSortDirection, + Proposal, + ProposalEndStatus, + Scalars, + Status, +} from 'generated/sdk'; import { ProposalViewData } from 'hooks/proposal/useProposalsCoreData'; import { FunctionType } from './utilTypes'; @@ -34,13 +39,13 @@ export const getUniqueArray = (array: (T | null)[]) => export const setSortDirectionOnSortField = ( columns: Column[], sortField: string | null | undefined, - sortDirection: string | null | undefined + sortDirection: PaginationSortDirection | null | undefined ) => { if (sortField !== undefined && sortField !== null && sortDirection) { const fieldIndex = columns.findIndex( (column) => column.field === sortField ); - columns[fieldIndex].defaultSort = sortDirection as SortDirectionType; + columns[fieldIndex].defaultSort = sortDirection; } else { columns.forEach((column) => (column.defaultSort = undefined)); } From f854fe5f1822f29066197ad9eee2bcbd266ff80e Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Thu, 15 Jan 2026 14:44:49 +0100 Subject: [PATCH 5/9] fix: handling json columns sorting without using raw queries --- .../postgres/ProposalDataSource.ts | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/backend/src/datasources/postgres/ProposalDataSource.ts b/apps/backend/src/datasources/postgres/ProposalDataSource.ts index 6e8f27f158..e2b60042d7 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -45,17 +45,16 @@ import { const fieldMap: { [key: string]: string } = { finalStatus: 'final_status', - 'technicalReviews.status': "technical_reviews->0->'status'", - 'technicalReviews.timeAllocation': "technical_reviews->0->'timeAllocation'", + 'technicalReviews.status': 'technical_review_status', + 'technicalReviews.timeAllocation': 'technical_review_time_allocation', // NOTE: For now sorting by first name only is completly fine because the full name is constructed from frist + last - technicalReviewAssigneesFullName: - "technical_reviews->0->'technicalReviewAssignee'->'firstname'", - 'faps.code': "faps->0->'code'", + technicalReviewAssigneesFullName: 'technical_review_assignee', + 'faps.code': 'fap_code', callShortCode: 'call_short_code', - 'instruments.name': "instruments->0->'name'", + 'instruments.name': 'instrument_name', statusName: 'proposal_status_id', 'instruments.managementTimeAllocation': - "instruments->0->'managementTimeAllocation'", + 'instrument_management_time_allocation', proposalId: 'proposal_id', title: 'title', submitted: 'submitted', @@ -392,7 +391,24 @@ export default class PostgresProposalDataSource implements ProposalDataSource { : []; return database - .select(['*', database.raw('count(*) OVER() AS full_count')]) + .select([ + '*', + database.raw("instruments->0->'name' AS instrument_name"), + database.raw( + "instruments->0->'managementTimeAllocation' AS instrument_management_time_allocation" + ), + database.raw( + "technical_reviews->0->'status' AS technical_review_status" + ), + database.raw( + "technical_reviews->0->'timeAllocation' AS technical_review_time_allocation" + ), + database.raw( + "technical_reviews->0->'technicalReviewAssignee'->'firstname' AS technical_review_assignee" + ), + database.raw("faps->0->'code' AS fap_code"), + database.raw('count(*) OVER() AS full_count'), + ]) .from('proposal_table_view') .join( 'users', From d57b8c7727b5b556e0ab49992261cf444796fe03 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Thu, 15 Jan 2026 15:10:37 +0100 Subject: [PATCH 6/9] fix: persistence of search query had been not made optional --- .../components/common/UserManagementTable.tsx | 1 + .../src/components/user/PeoplePage.tsx | 1 + .../src/components/user/PeopleTable.tsx | 87 +++++++++++-------- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/apps/frontend/src/components/common/UserManagementTable.tsx b/apps/frontend/src/components/common/UserManagementTable.tsx index b8fb64e5ef..aef9d95bbe 100644 --- a/apps/frontend/src/components/common/UserManagementTable.tsx +++ b/apps/frontend/src/components/common/UserManagementTable.tsx @@ -159,6 +159,7 @@ const UserManagementTable = ({ ? (user) => onUserAction('setPrincipalInvestigator', user) : undefined } + persistUrlQueryParams={true} /> {invites.length > 0 && ( diff --git a/apps/frontend/src/components/user/PeoplePage.tsx b/apps/frontend/src/components/user/PeoplePage.tsx index 6ec2bc6398..d81f9bd603 100644 --- a/apps/frontend/src/components/user/PeoplePage.tsx +++ b/apps/frontend/src/components/user/PeoplePage.tsx @@ -39,6 +39,7 @@ export default function PeoplePage() { id: user.id, }) } + persistUrlQueryParams={true} /> diff --git a/apps/frontend/src/components/user/PeopleTable.tsx b/apps/frontend/src/components/user/PeopleTable.tsx index 6a6fdffb1d..675a9d81f8 100644 --- a/apps/frontend/src/components/user/PeopleTable.tsx +++ b/apps/frontend/src/components/user/PeopleTable.tsx @@ -82,6 +82,7 @@ type PeopleTableProps = { setSelectedParticipants?: React.Dispatch< React.SetStateAction >; + persistUrlQueryParams?: boolean; }; const localColumns = [ @@ -180,6 +181,7 @@ const PeopleTable = ({ search, title, setPrincipalInvestigator, + persistUrlQueryParams = false, }: PeopleTableProps) => { const [query, setQuery] = useState<{ subtractUsers: number[]; @@ -211,7 +213,9 @@ const PeopleTable = ({ const tableRef = React.createRef>(); - const sortDirection = searchParams.get('sortDirection'); + const sortDirection = persistUrlQueryParams + ? searchParams.get('sortDirection') + : ''; useEffect(() => { if (!data) { return; @@ -529,7 +533,7 @@ const PeopleTable = ({ } columns={setSortDirectionOnSortField( columns ? columns : localColumns, - searchParams.get('sortField'), + persistUrlQueryParams ? searchParams.get('sortField') : '', sortDirection === PaginationSortDirection.ASC ? PaginationSortDirection.ASC : sortDirection === PaginationSortDirection.DESC @@ -539,65 +543,74 @@ const PeopleTable = ({ onSelectionChange={handleColumnSelectionChange} data={fetchRemoteUsersData} onPageChange={(page) => { - setSearchParams((searchParams) => { - searchParams.set('page', page.toString()); + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.set('page', page.toString()); - return searchParams; - }); + return searchParams; + }); }} onRowsPerPageChange={(pageSize) => { - setSearchParams((searchParams) => { - searchParams.set('pageSize', pageSize.toString()); - searchParams.set('page', '0'); + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.set('pageSize', pageSize.toString()); + searchParams.set('page', '0'); - return searchParams; - }); + return searchParams; + }); }} onSearchChange={(searchText) => { - setSearchParams((searchParams) => { - if (searchText) { - searchParams.set('search', searchText); - searchParams.set('page', '0'); - } else { - searchParams.delete('search'); - } + persistUrlQueryParams && + setSearchParams((searchParams) => { + if (searchText) { + searchParams.set('search', searchText); + searchParams.set('page', '0'); + } else { + searchParams.delete('search'); + } - return searchParams; - }); + return searchParams; + }); }} onOrderCollectionChange={(orderByCollection) => { const [orderBy] = orderByCollection; if (!orderBy) { - setSearchParams((searchParams) => { - searchParams.delete('sortField'); - searchParams.delete('sortDirection'); + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.delete('sortField'); + searchParams.delete('sortDirection'); - return searchParams; - }); + return searchParams; + }); } else { - setSearchParams((searchParams) => { - searchParams.set('sortField', orderBy.orderByField); - searchParams.set('sortDirection', orderBy.orderDirection); + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.set('sortField', orderBy.orderByField); + searchParams.set('sortDirection', orderBy.orderDirection); - return searchParams; - }); + return searchParams; + }); } }} options={{ search: search, - searchText: searchParams.get('search') || undefined, + searchText: persistUrlQueryParams + ? searchParams.get('search') || undefined + : 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, + pageSize: + persistUrlQueryParams && searchParams.get('pageSize') + ? +searchParams.get('pageSize')! + : undefined, + initialPage: + persistUrlQueryParams && searchParams.get('page') + ? +searchParams.get('page')! + : 0, ...mtOptions, selectionProps: (rowdata: BasicUserDetails) => ({ inputProps: { From 992eed49eacfea45064133d9fa7ae2115c4d001b Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Fri, 16 Jan 2026 12:59:59 +0100 Subject: [PATCH 7/9] fix: add persistqueryparams flag for flexibility --- .../src/components/user/PeopleTable.tsx | 86 +++++++++++-------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/apps/frontend/src/components/user/PeopleTable.tsx b/apps/frontend/src/components/user/PeopleTable.tsx index 048f38635d..dd8b4cab50 100644 --- a/apps/frontend/src/components/user/PeopleTable.tsx +++ b/apps/frontend/src/components/user/PeopleTable.tsx @@ -177,6 +177,7 @@ const PeopleTable = ({ onRemove, search, title, + persistUrlQueryParams = false, }: PeopleTableProps) => { const [query, setQuery] = useState<{ subtractUsers: number[]; @@ -208,7 +209,9 @@ const PeopleTable = ({ const tableRef = React.createRef>(); - const sortDirection = searchParams.get('sortDirection'); + const sortDirection = persistUrlQueryParams + ? searchParams.get('sortDirection') + : ''; useEffect(() => { if (!data) { return; @@ -498,7 +501,7 @@ const PeopleTable = ({ } columns={setSortDirectionOnSortField( columns ? columns : localColumns, - searchParams.get('sortField'), + persistUrlQueryParams ? searchParams.get('sortField') : '', sortDirection === PaginationSortDirection.ASC ? PaginationSortDirection.ASC : sortDirection === PaginationSortDirection.DESC @@ -508,65 +511,74 @@ const PeopleTable = ({ onSelectionChange={handleColumnSelectionChange} data={fetchRemoteUsersData} onPageChange={(page) => { - setSearchParams((searchParams) => { - searchParams.set('page', page.toString()); + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.set('page', page.toString()); - return searchParams; - }); + return searchParams; + }); }} onRowsPerPageChange={(pageSize) => { - setSearchParams((searchParams) => { - searchParams.set('pageSize', pageSize.toString()); - searchParams.set('page', '0'); + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.set('pageSize', pageSize.toString()); + searchParams.set('page', '0'); - return searchParams; - }); + return searchParams; + }); }} onSearchChange={(searchText) => { - setSearchParams((searchParams) => { - if (searchText) { - searchParams.set('search', searchText); - searchParams.set('page', '0'); - } else { - searchParams.delete('search'); - } + persistUrlQueryParams && + setSearchParams((searchParams) => { + if (searchText) { + searchParams.set('search', searchText); + searchParams.set('page', '0'); + } else { + searchParams.delete('search'); + } - return searchParams; - }); + return searchParams; + }); }} onOrderCollectionChange={(orderByCollection) => { const [orderBy] = orderByCollection; if (!orderBy) { - setSearchParams((searchParams) => { - searchParams.delete('sortField'); - searchParams.delete('sortDirection'); + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.delete('sortField'); + searchParams.delete('sortDirection'); - return searchParams; - }); + return searchParams; + }); } else { - setSearchParams((searchParams) => { - searchParams.set('sortField', orderBy.orderByField); - searchParams.set('sortDirection', orderBy.orderDirection); + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.set('sortField', orderBy.orderByField); + searchParams.set('sortDirection', orderBy.orderDirection); - return searchParams; - }); + return searchParams; + }); } }} options={{ search: search, - searchText: searchParams.get('search') || undefined, + searchText: persistUrlQueryParams + ? searchParams.get('search') || undefined + : 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, + pageSize: + persistUrlQueryParams && searchParams.get('pageSize') + ? +searchParams.get('pageSize')! + : undefined, + initialPage: + persistUrlQueryParams && searchParams.get('page') + ? +searchParams.get('page')! + : 0, ...mtOptions, selectionProps: (rowdata: BasicUserDetails) => ({ inputProps: { From 7d7b3030aa39d73481aa2d7273ae36ff4acf971c Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Mon, 19 Jan 2026 10:31:05 +0100 Subject: [PATCH 8/9] fix: fix filters on stfc datasource --- .../src/datasources/stfc/StfcProposalDataSource.ts | 4 ++-- apps/backend/src/datasources/stfc/StfcUserDataSource.ts | 8 ++++---- apps/backend/src/queries/UserQueries.spec.ts | 8 +++++--- apps/backend/src/resolvers/queries/UsersQuery.ts | 3 --- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts b/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts index c74d7bc3b0..25acddd231 100644 --- a/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts @@ -60,7 +60,7 @@ export default class StfcProposalDataSource extends PostgresProposalDataSource { const stfcUserIds: number[] = filter?.text ? [ ...( - await this.stfcUserDataSource.getUsers({ filter: filter.text }) + await this.stfcUserDataSource.getUsers({ searchText: filter.text }) ).users.map((user) => user.id), ] : []; @@ -231,7 +231,7 @@ export default class StfcProposalDataSource extends PostgresProposalDataSource { const stfcUserIds: number[] = searchText ? [ ...( - await this.stfcUserDataSource.getUsers({ filter: searchText }) + await this.stfcUserDataSource.getUsers({ searchText: searchText }) ).users.map((ids) => ids.id), ] : []; diff --git a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts index d222a07488..b1fc26896c 100644 --- a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts @@ -536,19 +536,19 @@ export class StfcUserDataSource implements UserDataSource { } async getUsers({ - filter, + searchText, first, offset, subtractUsers, }: UsersArgs): Promise<{ totalCount: number; users: BasicUserDetails[] }> { let userDetails: BasicUserDetails[] = []; - if (filter) { + if (searchText) { userDetails = []; const BasicPeopleByLastName: BasicPersonDetailsDTO[] | null = await UOWSClient.basicPersonDetails - .getBasicPersonDetails(undefined, filter, undefined) + .getBasicPersonDetails(undefined, searchText, undefined) .catch((error) => { logger.logError( 'An error occurred while fetching searchable person details using getBasicPersonDetails', @@ -575,7 +575,7 @@ export class StfcUserDataSource implements UserDataSource { } } else { const { users } = await postgresUserDataSource.getUsers({ - filter: undefined, + searchText: undefined, first: first, offset: offset, userRole: undefined, diff --git a/apps/backend/src/queries/UserQueries.spec.ts b/apps/backend/src/queries/UserQueries.spec.ts index c6373c0665..f514c5f8b4 100644 --- a/apps/backend/src/queries/UserQueries.spec.ts +++ b/apps/backend/src/queries/UserQueries.spec.ts @@ -48,7 +48,7 @@ describe('UserQueries', () => { test('A user officer is allowed to fetch all accounts', () => { return expect( - userQueries.getAll(dummyUserOfficerWithRole, { filter: '' }) + userQueries.getAll(dummyUserOfficerWithRole, { searchText: '' }) ).resolves.toStrictEqual({ totalCount: 2, users: [basicDummyUser, basicDummyUserNotOnProposal], @@ -57,7 +57,7 @@ describe('UserQueries', () => { test('A user is allowed to fetch all relevant accounts', () => { return expect( - userQueries.getAll(dummyUserWithRole, { filter: '' }) + userQueries.getAll(dummyUserWithRole, { searchText: '' }) ).resolves.toStrictEqual({ totalCount: 2, users: [basicDummyUser, basicDummyUserNotOnProposal], @@ -65,7 +65,9 @@ describe('UserQueries', () => { }); test('A user that is not logged in is not allowed to fetch all accounts', () => { - return expect(userQueries.getAll(null, { filter: '' })).resolves.toBe(null); + return expect(userQueries.getAll(null, { searchText: '' })).resolves.toBe( + null + ); }); test('A user is not allowed to fetch roles', () => { diff --git a/apps/backend/src/resolvers/queries/UsersQuery.ts b/apps/backend/src/resolvers/queries/UsersQuery.ts index 57ac0bd010..44c9fa0669 100644 --- a/apps/backend/src/resolvers/queries/UsersQuery.ts +++ b/apps/backend/src/resolvers/queries/UsersQuery.ts @@ -26,9 +26,6 @@ class UserQueryResult { @ArgsType() export class UsersArgs { - @Field(() => String, { nullable: true }) - filter?: string; - @Field(() => Int, { nullable: true }) first?: number; From 5c96ca4196a82519ae8d331142475262469a5257 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Mon, 19 Jan 2026 10:31:05 +0100 Subject: [PATCH 9/9] fix: fix filters on stfc datasource --- .../src/datasources/stfc/StfcProposalDataSource.ts | 4 ++-- apps/backend/src/datasources/stfc/StfcUserDataSource.ts | 8 ++++---- apps/backend/src/queries/UserQueries.spec.ts | 8 +++++--- apps/backend/src/resolvers/queries/UsersQuery.ts | 3 --- .../frontend/src/components/user/ProposalsPeopleTable.tsx | 2 +- .../src/graphql/user/getPreviousCollabortors.graphql | 4 ++-- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts b/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts index c74d7bc3b0..25acddd231 100644 --- a/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts @@ -60,7 +60,7 @@ export default class StfcProposalDataSource extends PostgresProposalDataSource { const stfcUserIds: number[] = filter?.text ? [ ...( - await this.stfcUserDataSource.getUsers({ filter: filter.text }) + await this.stfcUserDataSource.getUsers({ searchText: filter.text }) ).users.map((user) => user.id), ] : []; @@ -231,7 +231,7 @@ export default class StfcProposalDataSource extends PostgresProposalDataSource { const stfcUserIds: number[] = searchText ? [ ...( - await this.stfcUserDataSource.getUsers({ filter: searchText }) + await this.stfcUserDataSource.getUsers({ searchText: searchText }) ).users.map((ids) => ids.id), ] : []; diff --git a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts index d222a07488..b1fc26896c 100644 --- a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts @@ -536,19 +536,19 @@ export class StfcUserDataSource implements UserDataSource { } async getUsers({ - filter, + searchText, first, offset, subtractUsers, }: UsersArgs): Promise<{ totalCount: number; users: BasicUserDetails[] }> { let userDetails: BasicUserDetails[] = []; - if (filter) { + if (searchText) { userDetails = []; const BasicPeopleByLastName: BasicPersonDetailsDTO[] | null = await UOWSClient.basicPersonDetails - .getBasicPersonDetails(undefined, filter, undefined) + .getBasicPersonDetails(undefined, searchText, undefined) .catch((error) => { logger.logError( 'An error occurred while fetching searchable person details using getBasicPersonDetails', @@ -575,7 +575,7 @@ export class StfcUserDataSource implements UserDataSource { } } else { const { users } = await postgresUserDataSource.getUsers({ - filter: undefined, + searchText: undefined, first: first, offset: offset, userRole: undefined, diff --git a/apps/backend/src/queries/UserQueries.spec.ts b/apps/backend/src/queries/UserQueries.spec.ts index c6373c0665..f514c5f8b4 100644 --- a/apps/backend/src/queries/UserQueries.spec.ts +++ b/apps/backend/src/queries/UserQueries.spec.ts @@ -48,7 +48,7 @@ describe('UserQueries', () => { test('A user officer is allowed to fetch all accounts', () => { return expect( - userQueries.getAll(dummyUserOfficerWithRole, { filter: '' }) + userQueries.getAll(dummyUserOfficerWithRole, { searchText: '' }) ).resolves.toStrictEqual({ totalCount: 2, users: [basicDummyUser, basicDummyUserNotOnProposal], @@ -57,7 +57,7 @@ describe('UserQueries', () => { test('A user is allowed to fetch all relevant accounts', () => { return expect( - userQueries.getAll(dummyUserWithRole, { filter: '' }) + userQueries.getAll(dummyUserWithRole, { searchText: '' }) ).resolves.toStrictEqual({ totalCount: 2, users: [basicDummyUser, basicDummyUserNotOnProposal], @@ -65,7 +65,9 @@ describe('UserQueries', () => { }); test('A user that is not logged in is not allowed to fetch all accounts', () => { - return expect(userQueries.getAll(null, { filter: '' })).resolves.toBe(null); + return expect(userQueries.getAll(null, { searchText: '' })).resolves.toBe( + null + ); }); test('A user is not allowed to fetch roles', () => { diff --git a/apps/backend/src/resolvers/queries/UsersQuery.ts b/apps/backend/src/resolvers/queries/UsersQuery.ts index 57ac0bd010..44c9fa0669 100644 --- a/apps/backend/src/resolvers/queries/UsersQuery.ts +++ b/apps/backend/src/resolvers/queries/UsersQuery.ts @@ -26,9 +26,6 @@ class UserQueryResult { @ArgsType() export class UsersArgs { - @Field(() => String, { nullable: true }) - filter?: string; - @Field(() => Int, { nullable: true }) first?: number; diff --git a/apps/frontend/src/components/user/ProposalsPeopleTable.tsx b/apps/frontend/src/components/user/ProposalsPeopleTable.tsx index 7847470ced..b95d8dca57 100644 --- a/apps/frontend/src/components/user/ProposalsPeopleTable.tsx +++ b/apps/frontend/src/components/user/ProposalsPeopleTable.tsx @@ -283,7 +283,7 @@ const ProposalsPeopleTable = ({ const { previousCollaborators } = await api().getPreviousCollaborators({ userId: userId, - filter: tableQuery.search, + searchText: tableQuery.search, first: tableQuery.pageSize, offset: tableQuery.page * tableQuery.pageSize, subtractUsers: query.subtractUsers, diff --git a/apps/frontend/src/graphql/user/getPreviousCollabortors.graphql b/apps/frontend/src/graphql/user/getPreviousCollabortors.graphql index 845164e854..9adc119049 100644 --- a/apps/frontend/src/graphql/user/getPreviousCollabortors.graphql +++ b/apps/frontend/src/graphql/user/getPreviousCollabortors.graphql @@ -1,6 +1,6 @@ query getPreviousCollaborators( $userId: Int! - $filter: String + $searchText: String $first: Int $offset: Int $userRole: UserRole @@ -8,7 +8,7 @@ query getPreviousCollaborators( ) { previousCollaborators( userId: $userId - filter: $filter + searchText: $searchText first: $first offset: $offset userRole: $userRole