diff --git a/apps/backend/src/datasources/UserDataSource.ts b/apps/backend/src/datasources/UserDataSource.ts index 29c9447fa7..2f34a26f93 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 7e49445120..730dc0f9d7 100644 --- a/apps/backend/src/datasources/mockups/UserDataSource.ts +++ b/apps/backend/src/datasources/mockups/UserDataSource.ts @@ -442,9 +442,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 b61884d6c2..87000b1c37 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') @@ -494,26 +502,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) { @@ -530,8 +537,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( @@ -550,14 +561,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); @@ -574,14 +595,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/datasources/stfc/StfcUserDataSource.ts b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts index 82031914b1..70b06ccb5f 100644 --- a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts @@ -588,7 +588,7 @@ export class StfcUserDataSource implements UserDataSource { offset: offset, userRole: undefined, subtractUsers: subtractUsers, - orderDirection: 'asc', + sortDirection: 'asc', }); if (users[0]) { @@ -610,18 +610,22 @@ export class StfcUserDataSource implements UserDataSource { async getPreviousCollaborators( userId: number, - filter?: string, first?: number, offset?: number, - userRole?: number, + sortField?: string, + sortDirection?: string, + searchText?: string, + userRole?: UserRole, subtractUsers?: [number] ): Promise<{ totalCount: number; users: BasicUserDetails[] }> { const dbUsers: BasicUserDetails[] = ( await postgresUserDataSource.getPreviousCollaborators( userId, - filter, first, offset, + sortField, + sortDirection, + searchText, undefined, subtractUsers ) diff --git a/apps/backend/src/queries/UserQueries.ts b/apps/backend/src/queries/UserQueries.ts index f35b9c3fea..8c55fa7c76 100644 --- a/apps/backend/src/queries/UserQueries.ts +++ b/apps/backend/src/queries/UserQueries.ts @@ -140,17 +140,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/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 && ( diff --git a/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx b/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx index 2120f34a85..29f93011cd 100644 --- a/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx +++ b/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx @@ -82,7 +82,10 @@ const CreateUpdateInstrument = ({ try { await api() - .getUsers({ filter: value, userRole: UserRole.INSTRUMENT_SCIENTIST }) + .getUsers({ + userRole: UserRole.INSTRUMENT_SCIENTIST, + searchText: value, + }) .then((data) => { if (data.users?.totalCount == 0) { setFieldError( diff --git a/apps/frontend/src/components/internalReview/CreateUpdateInternalReview.tsx b/apps/frontend/src/components/internalReview/CreateUpdateInternalReview.tsx index 66beff68ed..6758d1a25f 100644 --- a/apps/frontend/src/components/internalReview/CreateUpdateInternalReview.tsx +++ b/apps/frontend/src/components/internalReview/CreateUpdateInternalReview.tsx @@ -82,7 +82,7 @@ const CreateUpdateInternalReview = ({ try { await api() - .getUsers({ filter: value, userRole: UserRole.INTERNAL_REVIEWER }) + .getUsers({ searchText: value, userRole: UserRole.INTERNAL_REVIEWER }) .then((data) => { if (data.users?.totalCount == 0) { setFieldError( diff --git a/apps/frontend/src/components/proposal/ParticipantSelector.tsx b/apps/frontend/src/components/proposal/ParticipantSelector.tsx index 890edd7f57..daf10c88a1 100644 --- a/apps/frontend/src/components/proposal/ParticipantSelector.tsx +++ b/apps/frontend/src/components/proposal/ParticipantSelector.tsx @@ -154,8 +154,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