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 9bffd6246d..2b1703dba4 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[] }>; getInvitedProposal(inviteId: number): Promise; diff --git a/apps/backend/src/datasources/UserDataSource.ts b/apps/backend/src/datasources/UserDataSource.ts index 23f02f6cdb..0d293ed71d 100644 --- a/apps/backend/src/datasources/UserDataSource.ts +++ b/apps/backend/src/datasources/UserDataSource.ts @@ -5,6 +5,7 @@ import { BasicUserDetails, User, UserRole } from '../models/User'; import { AddUserRoleArgs } from '../resolvers/mutations/AddUserRoleMutation'; import { UpdateUserByIdArgs } from '../resolvers/mutations/UpdateUserMutation'; import { UsersArgs } from '../resolvers/queries/UsersQuery'; +import { PaginationSortDirection } from '../utils/pagination'; export interface UserDataSource { delete(id: number): Promise; @@ -35,9 +36,11 @@ export interface UserDataSource { ): Promise<{ totalCount: number; users: BasicUserDetails[] }>; getPreviousCollaborators( user_id: number, - filter?: string, first?: number, offset?: number, + sortField?: string, + sortDirection?: PaginationSortDirection, + searchText?: string, userRole?: UserRole, subtractUsers?: [number] ): Promise<{ totalCount: number; users: BasicUserDetails[] }>; 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 f9223826ea..8ef0ba7a80 100644 --- a/apps/backend/src/datasources/mockups/ProposalDataSource.ts +++ b/apps/backend/src/datasources/mockups/ProposalDataSource.ts @@ -15,6 +15,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'; @@ -143,7 +144,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: [] }; @@ -430,7 +431,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 9024e6149a..7b065d525b 100644 --- a/apps/backend/src/datasources/mockups/UserDataSource.ts +++ b/apps/backend/src/datasources/mockups/UserDataSource.ts @@ -10,6 +10,7 @@ import { import { AddUserRoleArgs } from '../../resolvers/mutations/AddUserRoleMutation'; import { UpdateUserByIdArgs } from '../../resolvers/mutations/UpdateUserMutation'; import { UsersArgs } from '../../resolvers/queries/UsersQuery'; +import { PaginationSortDirection } from '../../utils/pagination'; import { UserDataSource } from '../UserDataSource'; export const basicDummyUser = new BasicUserDetails( @@ -390,9 +391,13 @@ export class UserDataSourceMock implements UserDataSource { async getPreviousCollaborators( user_id: number, - filter?: string, first?: number, - offset?: number + offset?: number, + sortField?: string, + sortDirection?: PaginationSortDirection, + searchText?: string, + userRole?: UserRole, + subtractUsers?: [number] ): Promise<{ totalCount: number; users: BasicUserDetails[] }> { return { totalCount: 2, 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 0e4090d8af..31c07b4b2f 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'; @@ -46,17 +47,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', @@ -384,7 +384,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[] }> { @@ -393,7 +393,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', @@ -461,7 +478,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) { @@ -1069,7 +1086,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 a37d6999ec..b7e75c5eb5 100644 --- a/apps/backend/src/datasources/postgres/UserDataSource.ts +++ b/apps/backend/src/datasources/postgres/UserDataSource.ts @@ -15,6 +15,7 @@ import { import { AddUserRoleArgs } from '../../resolvers/mutations/AddUserRoleMutation'; import { UpdateUserByIdArgs } 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 { @@ -29,6 +30,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') @@ -376,25 +385,25 @@ export default class PostgresUserDataSource implements UserDataSource { } async getUsers({ - filter, + searchText, first, offset, userRole, subtractUsers, - orderBy, - orderDirection = 'desc', + sortField = 'created_at', + sortDirection, }: 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' }) .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) { @@ -411,8 +420,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.orderBy(sortField, sortDirection); } }) .then( @@ -431,14 +444,24 @@ export default class PostgresUserDataSource implements UserDataSource { async getPreviousCollaborators( userId: number, - filter?: string, first?: number, offset?: number, + sortField?: string, + sortDirection?: PaginationSortDirection, + 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); @@ -455,14 +478,22 @@ 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); }); } + if (sortField && sortDirection) { + if (!fieldMap.hasOwnProperty(sortField)) { + throw new GraphQLError(`Bad sort field given: ${sortField}`); + } + sortField = fieldMap[sortField]; + query.orderBy(sortField, sortDirection); + } + if (first) { query.limit(first); } @@ -553,7 +584,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..25acddd231 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'; @@ -59,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), ] : []; @@ -224,13 +225,13 @@ 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 ? [ ...( - await this.stfcUserDataSource.getUsers({ filter: searchText }) + await this.stfcUserDataSource.getUsers({ searchText: searchText }) ).users.map((ids) => ids.id), ] : []; @@ -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 d56027d6e5..c8a71ecd0c 100644 --- a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts @@ -11,6 +11,7 @@ import { AddUserRoleArgs } from '../../resolvers/mutations/AddUserRoleMutation'; import { UpdateUserByIdArgs } 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'; @@ -519,19 +520,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', @@ -558,12 +559,12 @@ export class StfcUserDataSource implements UserDataSource { } } else { const { users } = await postgresUserDataSource.getUsers({ - filter: undefined, + searchText: undefined, first: first, offset: offset, userRole: undefined, subtractUsers: subtractUsers, - orderDirection: 'asc', + sortDirection: PaginationSortDirection.asc, }); if (users[0]) { @@ -585,18 +586,22 @@ export class StfcUserDataSource implements UserDataSource { async getPreviousCollaborators( userId: number, - filter?: string, first?: number, offset?: number, - userRole?: number, + sortField?: string, + sortDirection?: PaginationSortDirection, + 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/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 83a7a10014..edff6d5785 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.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/queries/UserQueries.ts b/apps/backend/src/queries/UserQueries.ts index f761b98b3a..6cf57417a4 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 { @@ -136,17 +137,21 @@ export default class UserQueries { async getPreviousCollaborators( agent: UserWithRole | null, userId: number, - filter?: string, first?: number, offset?: number, + sortField?: string, + sortDirection?: PaginationSortDirection, + 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/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 b1c0c84496..44c9fa0669 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() @@ -25,9 +26,6 @@ class UserQueryResult { @ArgsType() export class UsersArgs { - @Field(() => String, { nullable: true }) - filter?: string; - @Field(() => Int, { nullable: true }) first?: number; @@ -40,11 +38,14 @@ export class UsersArgs { @Field(() => [Int], { nullable: 'itemsAndList' }) subtractUsers?: [number]; - @Field(() => String, { nullable: true }) - orderBy?: string; + @Field({ nullable: true }) + public sortField?: string; + + @Field(() => PaginationSortDirection, { nullable: true }) + public sortDirection?: PaginationSortDirection; - @Field(() => String, { nullable: true }) - orderDirection?: string; + @Field({ nullable: true }) + public searchText?: string; } @ArgsType() @@ -65,20 +66,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/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 53c3f35ff8..a909649c8a 100644 --- a/apps/frontend/src/components/experiment/ExperimentsTable.tsx +++ b/apps/frontend/src/components/experiment/ExperimentsTable.tsx @@ -5,11 +5,12 @@ 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'; import ExperimentReviewContent, { @@ -58,11 +59,12 @@ export default function ExperimentsTable({ const page = searchParams.get('page'); const pageSize = searchParams.get('pageSize'); const selectedExperimentId = searchParams.get('experiment'); - const refreshTableData = () => { tableRef.current?.onQueryChange({}); }; + const isFirstRender = useRef(true); + React.useEffect(() => { setSelectedExperiment( tableData.find( @@ -73,15 +75,13 @@ export default function ExperimentsTable({ }, [selectedExperimentId, tableData]); React.useEffect(() => { - let isMounted = true; + if (isFirstRender.current) { + isFirstRender.current = false; - if (isMounted) { - refreshTableData(); + return; } + refreshTableData(); - return () => { - isMounted = false; - }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(experimentsFilter)]); @@ -111,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, @@ -192,6 +197,17 @@ export default function ExperimentsTable({ ...columns, ]; } + const sortDirection = searchParams.get('sortDirection'); + + columns = setSortDirectionOnSortField( + columns, + searchParams.get('sortField'), + sortDirection == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined + ); const experimentReviewTabs = [ EXPERIMENT_MODAL_TAB_NAMES.EXPERIMENT_INFORMATION, @@ -215,12 +231,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 +260,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 b746de4fa2..43fb554e5b 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/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 5d0564bf50..2340db453d 100644 --- a/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx +++ b/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx @@ -53,6 +53,7 @@ import { FapInstrumentInput, FeatureId, InstrumentMinimalFragment, + PaginationSortDirection, ProposalViewInstrument, ProposalsFilter, Status, @@ -651,10 +652,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'); @@ -704,7 +711,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 330294700b..4dc9e19fe7 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, @@ -177,7 +178,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/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 cb8dc6b7c4..d6bdba468e 100644 --- a/apps/frontend/src/components/user/PeopleTable.tsx +++ b/apps/frontend/src/components/user/PeopleTable.tsx @@ -10,6 +10,7 @@ import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; import { Formik } from 'formik'; import React, { useState, useEffect, useContext } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { ActionButtonContainer } from 'components/common/ActionButtonContainer'; import MaterialTable from 'components/common/DenseMaterialTable'; @@ -25,8 +26,10 @@ import { Maybe, getSdk, BasicUserDetailsFragment, + PaginationSortDirection, } from 'generated/sdk'; import { useDataApi } from 'hooks/common/useDataApi'; +import { setSortDirectionOnSortField } from 'utils/helperFunctions'; import { tableIcons } from 'utils/materialIcons'; import { FunctionType } from 'utils/utilTypes'; @@ -70,6 +73,7 @@ type PeopleTableProps = { setSelectedParticipants?: React.Dispatch< React.SetStateAction >; + persistUrlQueryParams?: boolean; }; const localColumns = [ @@ -143,6 +147,7 @@ const PeopleTable = ({ onRemove, search, title, + persistUrlQueryParams = false, }: PeopleTableProps) => { const [query, setQuery] = useState<{ subtractUsers: number[]; @@ -161,9 +166,13 @@ const PeopleTable = ({ const [currentPageIds, setCurrentPageIds] = useState([]); const [invitedUsers, setInvitedUsers] = useState([]); const [tableEmails, setTableEmails] = useState([]); + const [searchParams, setSearchParams] = useSearchParams(); const tableRef = React.createRef>(); + const sortDirection = persistUrlQueryParams + ? searchParams.get('sortDirection') + : ''; useEffect(() => { if (!data) { return; @@ -247,13 +256,18 @@ 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 == PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : orderBy?.orderDirection == PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined, + searchText: tableQuery.search, }); const filteredData = data @@ -371,16 +385,86 @@ const PeopleTable = ({ {title} } - columns={columns ?? localColumns} + columns={setSortDirectionOnSortField( + columns ? columns : localColumns, + persistUrlQueryParams ? searchParams.get('sortField') : '', + sortDirection === PaginationSortDirection.ASC + ? PaginationSortDirection.ASC + : sortDirection === PaginationSortDirection.DESC + ? PaginationSortDirection.DESC + : undefined + )} onSelectionChange={handleColumnSelectionChange} data={fetchRemoteUsersData} + onPageChange={(page) => { + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.set('page', page.toString()); + + return searchParams; + }); + }} + onRowsPerPageChange={(pageSize) => { + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.set('pageSize', pageSize.toString()); + searchParams.set('page', '0'); + + return searchParams; + }); + }} + onSearchChange={(searchText) => { + persistUrlQueryParams && + 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) { + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.delete('sortField'); + searchParams.delete('sortDirection'); + + return searchParams; + }); + } else { + persistUrlQueryParams && + setSearchParams((searchParams) => { + searchParams.set('sortField', orderBy.orderByField); + searchParams.set('sortDirection', orderBy.orderDirection); + + return searchParams; + }); + } + }} options={{ search: search, + searchText: persistUrlQueryParams + ? searchParams.get('search') || undefined + : undefined, debounceInterval: 400, selection: selection, headerSelectionProps: { inputProps: { 'aria-label': 'Select All Rows' }, }, + pageSize: + persistUrlQueryParams && searchParams.get('pageSize') + ? +searchParams.get('pageSize')! + : undefined, + initialPage: + persistUrlQueryParams && searchParams.get('page') + ? +searchParams.get('page')! + : 0, ...mtOptions, selectionProps: (rowdata: BasicUserDetails) => ({ inputProps: { diff --git a/apps/frontend/src/components/user/ProposalsPeopleTable.tsx b/apps/frontend/src/components/user/ProposalsPeopleTable.tsx index 40350b2231..4797cf6210 100644 --- a/apps/frontend/src/components/user/ProposalsPeopleTable.tsx +++ b/apps/frontend/src/components/user/ProposalsPeopleTable.tsx @@ -256,7 +256,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/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/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 diff --git a/apps/frontend/src/graphql/user/getUsers.graphql b/apps/frontend/src/graphql/user/getUsers.graphql index ffed748e5f..298a73ab09 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: PaginationSortDirection + $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 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)); }