diff --git a/apps/backend/src/datasources/postgres/ProposalDataSource.ts b/apps/backend/src/datasources/postgres/ProposalDataSource.ts index 7dd88ec37e..5dac6b4300 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -23,6 +23,7 @@ import { ProposalDataSource } from '../ProposalDataSource'; import { TagDataSource } from '../TagDataSource'; import { WorkflowDataSource } from '../WorkflowDataSource'; import { + InstrumentFilterInput, ProposalsFilter, QuestionFilterInput, } from './../../resolvers/queries/ProposalsQuery'; @@ -94,6 +95,29 @@ export async function calculateReferenceNumber( return prefix + paddedSequence; } +/** + * Resolves instrument IDs from an InstrumentFilterInput. + * Supports both `instrumentIds` and `instrumentId`(deprecated). + */ +export function resolveInstrumentIds( + instrumentFilter?: InstrumentFilterInput +): number[] | undefined { + if (!instrumentFilter) { + return undefined; + } + if ( + instrumentFilter.instrumentIds && + instrumentFilter.instrumentIds.length > 0 + ) { + return instrumentFilter.instrumentIds; + } + if (instrumentFilter.instrumentId) { + return [instrumentFilter.instrumentId]; + } + + return undefined; +} + @injectable() export default class PostgresProposalDataSource implements ProposalDataSource { constructor( @@ -471,11 +495,20 @@ export default class PostgresProposalDataSource implements ProposalDataSource { if (filter?.instrumentFilter?.showMultiInstrumentProposals) { query.whereRaw('jsonb_array_length(instruments) > 1'); - } else if (filter?.instrumentFilter?.instrumentId) { - query.whereRaw( - 'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')', - { instrumentId: filter.instrumentFilter.instrumentId } + } else { + const effectiveInstrumentIds = resolveInstrumentIds( + filter?.instrumentFilter ); + if (effectiveInstrumentIds && effectiveInstrumentIds.length > 0) { + query.where(function () { + effectiveInstrumentIds.forEach((id) => { + this.orWhereRaw( + 'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')', + { instrumentId: id } + ); + }); + }); + } } if (filter?.proposalStatusId) { @@ -626,16 +659,19 @@ export default class PostgresProposalDataSource implements ProposalDataSource { ); } - if (filter?.instrumentFilter?.instrumentId) { + const effectiveInstrumentIds = resolveInstrumentIds( + filter?.instrumentFilter + ); + if (effectiveInstrumentIds && effectiveInstrumentIds.length > 0) { query .leftJoin( 'instrument_has_proposals', 'instrument_has_proposals.proposal_pk', 'proposals.proposal_pk' ) - .where( + .whereIn( 'instrument_has_proposals.instrument_id', - filter.instrumentFilter.instrumentId + effectiveInstrumentIds ); } @@ -782,12 +818,21 @@ export default class PostgresProposalDataSource implements ProposalDataSource { if (filter?.instrumentFilter?.showMultiInstrumentProposals) { query.whereRaw('jsonb_array_length(instruments) > 1'); - } else if (filter?.instrumentFilter?.instrumentId) { - // NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with id equal to filter.instrumentId - query.whereRaw( - 'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')', - { instrumentId: filter.instrumentFilter?.instrumentId } + } else { + const effectiveInstrumentIds = resolveInstrumentIds( + filter?.instrumentFilter ); + if (effectiveInstrumentIds && effectiveInstrumentIds.length > 0) { + // NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with id equal to filter.instrumentId + query.where(function () { + effectiveInstrumentIds.forEach((id) => { + this.orWhereRaw( + 'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')', + { instrumentId: id } + ); + }); + }); + } } if (filter?.proposalStatusId) { @@ -1108,14 +1153,15 @@ export default class PostgresProposalDataSource implements ProposalDataSource { 'ins.instrument_id' ) .modify((query) => { - const instrumentId = filter?.instrumentFilter?.instrumentId; + const effectiveInstrumentIds = resolveInstrumentIds( + filter?.instrumentFilter + ); - if (instrumentId && !isNaN(instrumentId)) { + if (effectiveInstrumentIds && effectiveInstrumentIds.length > 0) { query.join('instrument_has_proposals as ihp', function () { - this.on('ihp.proposal_pk', '=', 'proposals.proposal_pk').andOnVal( + this.on('ihp.proposal_pk', '=', 'proposals.proposal_pk').andOnIn( 'ihp.instrument_id', - '=', - instrumentId + effectiveInstrumentIds ); }); } diff --git a/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts b/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts index 1ad9e102f4..d5e79954cc 100644 --- a/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts @@ -10,9 +10,14 @@ 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 { StfcUserDataSource } from './StfcUserDataSource'; import PostgresAdminDataSource from '../postgres/AdminDataSource'; import PostgresCallDataSource from '../postgres/CallDataSource'; import database from '../postgres/database'; +import PostgresProposalDataSource, { + resolveInstrumentIds, +} from '../postgres/ProposalDataSource'; import { CallRecord, createCallObject, @@ -24,8 +29,6 @@ import PostgresTagDataSource from '../postgres/TagDataSource'; import PostgresUserDataSource from '../postgres/UserDataSource'; import PostgresWorkflowDataSource from '../postgres/WorkflowDataSource'; import { ProposalsFilter } from './../../resolvers/queries/ProposalsQuery'; -import PostgresProposalDataSource from './../postgres/ProposalDataSource'; -import { StfcUserDataSource } from './StfcUserDataSource'; const postgresProposalDataSource = new PostgresProposalDataSource( new PostgresWorkflowDataSource(new PostgresStatusDataSource()), @@ -163,12 +166,22 @@ export default class StfcProposalDataSource extends PostgresProposalDataSource { } if (filter?.instrumentFilter?.showMultiInstrumentProposals) { query.whereRaw('jsonb_array_length(instruments) > 1'); - } else if (filter?.instrumentFilter?.instrumentId) { - // NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with id equal to filter.instrumentId - query.whereRaw( - 'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')', - { instrumentId: filter?.instrumentFilter?.instrumentId } + } else { + const effectiveInstrumentIds = resolveInstrumentIds( + filter?.instrumentFilter ); + + if (effectiveInstrumentIds && effectiveInstrumentIds.length > 0) { + // NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with id equal to filter.instrumentId + query.where(function () { + effectiveInstrumentIds.forEach((id) => { + this.orWhereRaw( + 'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')', + { instrumentId: id } + ); + }); + }); + } } if (filter?.proposalStatusId) { diff --git a/apps/backend/src/resolvers/queries/ProposalsQuery.ts b/apps/backend/src/resolvers/queries/ProposalsQuery.ts index ac939d844d..50b558c452 100644 --- a/apps/backend/src/resolvers/queries/ProposalsQuery.ts +++ b/apps/backend/src/resolvers/queries/ProposalsQuery.ts @@ -33,8 +33,15 @@ export class QuestionFilterInput { @InputType() export class InstrumentFilterInput { - @Field(() => Int, { nullable: true }) - public instrumentId: number; + /** @deprecated Use instrumentIds instead */ + @Field(() => Int, { + nullable: true, + deprecationReason: 'Use instrumentIds instead', + }) + public instrumentId?: number; + + @Field(() => [Int], { nullable: true }) + public instrumentIds?: number[]; @Field(() => Boolean) public showMultiInstrumentProposals: boolean; diff --git a/apps/e2e/cypress/e2e/FAPs.cy.ts b/apps/e2e/cypress/e2e/FAPs.cy.ts index 005868adba..e6e11f1910 100644 --- a/apps/e2e/cypress/e2e/FAPs.cy.ts +++ b/apps/e2e/cypress/e2e/FAPs.cy.ts @@ -572,6 +572,7 @@ context('Fap reviews tests', () => { cy.get('[data-cy=instrument-filter]').click(); cy.get('[role=presentation]').contains(instrument.name).click(); + cy.get('body').type('{esc}'); cy.get('[data-cy="fap-assignments-table"]').contains(instrument.name); cy.get('[data-cy="fap-assignments-table"]').contains( diff --git a/apps/e2e/cypress/e2e/instruments.cy.ts b/apps/e2e/cypress/e2e/instruments.cy.ts index 401692a160..a8d2005d59 100644 --- a/apps/e2e/cypress/e2e/instruments.cy.ts +++ b/apps/e2e/cypress/e2e/instruments.cy.ts @@ -27,6 +27,11 @@ context('Instrument tests', () => { abstract: faker.random.words(5), }; + const proposal3 = { + title: faker.random.words(2), + abstract: faker.random.words(5), + }; + const scientist1 = initialDBData.users.user1; const scientist2 = initialDBData.users.user2; @@ -801,6 +806,29 @@ context('Instrument tests', () => { } } ); + + cy.createProposal({ callId: initialDBData.call.id }).then( + ({ createProposal }) => { + if (createProposal) { + cy.updateProposal({ + proposalPk: createProposal.primaryKey, + title: proposal3.title, + abstract: proposal3.abstract, + }); + + cy.assignProposalsToInstruments({ + proposalPks: [createProposal.primaryKey], + instrumentIds: [createInstrument.id], + }); + + cy.updateTechnicalReviewAssignee({ + proposalPks: [createProposal.primaryKey], + userId: scientist2.id, + instrumentId: createInstrument.id, + }); + } + } + ); } }); @@ -808,28 +836,49 @@ context('Instrument tests', () => { cy.contains(proposal1.title); cy.contains(proposal2.title); + cy.contains(proposal3.title); cy.get('[data-cy="instrument-filter"]').click(); - cy.get('[data-value="multi"]').click(); + cy.get('[role="listbox"]').contains('Multiple').click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.get('table.MuiTable-root tbody tr').should( 'not.contain', proposal1.title ); + cy.get('table.MuiTable-root tbody tr').should( + 'not.contain', + proposal3.title + ); cy.contains(proposal2.title); cy.contains(instrument1.name); cy.contains(instrument2.name); cy.get('[data-cy="instrument-filter"]').click(); cy.get('[role="listbox"]').contains(instrument1.name).click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.contains(proposal1.title); cy.contains(proposal2.title); + cy.get('table.MuiTable-root tbody tr').should( + 'not.contain', + proposal3.title + ); cy.get('[data-cy="instrument-filter"]').click(); cy.get('[role="listbox"]').contains(instrument2.name).click(); + cy.get('body').type('{esc}'); + cy.finishedLoading(); + + cy.contains(proposal1.title); + cy.contains(proposal2.title); + cy.contains(proposal3.title); + + cy.get('[data-cy="instrument-filter"]').click(); + cy.get('[role="listbox"]').contains(instrument1.name).click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.get('table.MuiTable-root tbody tr').should( @@ -837,6 +886,16 @@ context('Instrument tests', () => { proposal1.title ); cy.contains(proposal2.title); + cy.contains(proposal3.title); + + cy.get('[data-cy="instrument-filter"]').click(); + cy.get('[role="listbox"]').contains('All').click(); + cy.get('body').type('{esc}'); + cy.finishedLoading(); + + cy.contains(proposal1.title); + cy.contains(proposal2.title); + cy.contains(proposal3.title); }); it('Officer should be able to update all un-assigned technical reviews to new contact', () => { @@ -1163,32 +1222,78 @@ context('Instrument tests', () => { } } ); + + cy.createProposal({ callId: initialDBData.call.id }).then( + ({ createProposal }) => { + if (createProposal) { + cy.updateProposal({ + proposalPk: createProposal.primaryKey, + title: proposal3.title, + abstract: proposal3.abstract, + }); + + cy.assignProposalsToInstruments({ + proposalPks: [createProposal.primaryKey], + instrumentIds: [createdInstrument2Id], + }); + + cy.updateTechnicalReviewAssignee({ + proposalPks: [createProposal.primaryKey], + userId: scientist2.id, + instrumentId: createdInstrument2Id, + }); + } + } + ); + cy.reload(); + cy.contains('Proposals'); selectAllProposalsFilterStatus(); cy.contains(proposal1.title); cy.contains(proposal2.title); + cy.contains(proposal3.title); cy.get('[data-cy="instrument-filter"]').click(); - cy.get('[data-value="multi"]').click(); + cy.get('[role="listbox"]').contains('Multiple').click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.get('table.MuiTable-root tbody tr').should( 'not.contain', proposal1.title ); + cy.get('table.MuiTable-root tbody tr').should( + 'not.contain', + proposal3.title + ); cy.contains(proposal2.title); cy.get('[data-cy="instrument-filter"]').click(); cy.get('[role="listbox"]').contains(instrument1.name).click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.contains(proposal1.title); cy.contains(proposal2.title); + cy.get('table.MuiTable-root tbody tr').should( + 'not.contain', + proposal3.title + ); cy.get('[data-cy="instrument-filter"]').click(); cy.get('[role="listbox"]').contains(instrument2.name).click(); + cy.get('body').type('{esc}'); + cy.finishedLoading(); + + cy.contains(proposal1.title); + cy.contains(proposal2.title); + cy.contains(proposal3.title); + + cy.get('[data-cy="instrument-filter"]').click(); + cy.get('[role="listbox"]').contains(instrument1.name).click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.get('table.MuiTable-root tbody tr').should( @@ -1196,6 +1301,16 @@ context('Instrument tests', () => { proposal1.title ); cy.contains(proposal2.title); + cy.contains(proposal3.title); + + cy.get('[data-cy="instrument-filter"]').click(); + cy.get('[role="listbox"]').contains('All').click(); + cy.get('body').type('{esc}'); + cy.finishedLoading(); + + cy.contains(proposal1.title); + cy.contains(proposal2.title); + cy.contains(proposal3.title); }); it('Instrument scientist should have a call and instrument filter', () => { @@ -1215,6 +1330,7 @@ context('Instrument tests', () => { cy.get('[data-cy="instrument-filter"]').click(); cy.get('[role="listbox"]').contains(instrument2.name).click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.contains('No records to display'); @@ -1222,6 +1338,7 @@ context('Instrument tests', () => { cy.get('[data-cy="instrument-filter"]').click(); cy.get('[role="listbox"]').contains('All').click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.contains(proposal1.title); diff --git a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts index 8ffeff06ca..88b0323bb6 100644 --- a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts +++ b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts @@ -700,6 +700,7 @@ context('Technique Proposal tests', () => { }); cy.get('[role="listbox"]').contains(instrument1.name).click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.contains(proposal1.title); @@ -723,6 +724,7 @@ context('Technique Proposal tests', () => { }); cy.get('[role="listbox"]').contains(instrument2.name).click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.contains(proposal3.title); @@ -739,6 +741,7 @@ context('Technique Proposal tests', () => { cy.get('[data-cy="instrument-filter"]').click(); cy.get('[role="listbox"]').contains('All').click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.contains(proposal1.title); @@ -980,6 +983,7 @@ context('Technique Proposal tests', () => { }); cy.get('[role="listbox"]').contains(instrument2.name).click(); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.contains(proposal2.title); diff --git a/apps/frontend/src/components/common/experimentFilters/InstrumentFilter.tsx b/apps/frontend/src/components/common/experimentFilters/InstrumentFilter.tsx index 3a57c79a00..5562b5e2c9 100644 --- a/apps/frontend/src/components/common/experimentFilters/InstrumentFilter.tsx +++ b/apps/frontend/src/components/common/experimentFilters/InstrumentFilter.tsx @@ -26,13 +26,13 @@ type InstrumentFilterProps = { shouldShowAll?: boolean; shouldShowMultiple?: boolean; showMultiInstrumentProposals?: boolean; - instrumentId?: number | null; + instrumentIds?: number[] | null; }; const InstrumentFilter = ({ instruments, isLoading, - instrumentId, + instrumentIds, onChange, shouldShowAll, shouldShowMultiple, @@ -63,7 +63,7 @@ const InstrumentFilter = ({ aria-labelledby="instrument-select-label" onChange={(e) => { const newValue: InstrumentFilterInput = { - instrumentId: null, + instrumentIds: null, showMultiInstrumentProposals: false, showAllProposals: false, }; @@ -83,20 +83,20 @@ const InstrumentFilter = ({ e.target.value === InstrumentFilterEnum.ALL || e.target.value === InstrumentFilterEnum.MULTI ) { - newValue.instrumentId = null; + newValue.instrumentIds = null; newValue.showMultiInstrumentProposals = e.target.value === InstrumentFilterEnum.MULTI; newValue.showAllProposals = e.target.value === InstrumentFilterEnum.ALL; } else { - newValue.instrumentId = +e.target.value; + newValue.instrumentIds = [+e.target.value]; } onChange?.(newValue); }} value={ showMultiInstrumentProposals ? InstrumentFilterEnum.MULTI - : instrumentId || InstrumentFilterEnum.ALL + : instrumentIds?.[0] || InstrumentFilterEnum.ALL } data-cy="instrument-filter" > diff --git a/apps/frontend/src/components/common/proposalFilters/InstrumentFilter.tsx b/apps/frontend/src/components/common/proposalFilters/InstrumentFilter.tsx index 3a57c79a00..36cab41101 100644 --- a/apps/frontend/src/components/common/proposalFilters/InstrumentFilter.tsx +++ b/apps/frontend/src/components/common/proposalFilters/InstrumentFilter.tsx @@ -1,10 +1,12 @@ import Box from '@mui/material/Box'; +import Checkbox from '@mui/material/Checkbox'; import Divider from '@mui/material/Divider'; import FormControl from '@mui/material/FormControl'; import InputLabel from '@mui/material/InputLabel'; +import ListItemText from '@mui/material/ListItemText'; import ListSubheader from '@mui/material/ListSubheader'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; import React, { Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; @@ -19,6 +21,33 @@ export enum InstrumentFilterEnum { MULTI = 'multi', } +export const parseInstrumentQuery = ( + instrumentQuery: string | null | undefined +): number[] | null => { + if ( + instrumentQuery == null || + instrumentQuery === InstrumentFilterEnum.MULTI || + instrumentQuery === InstrumentFilterEnum.ALL + ) { + return null; + } + + return instrumentQuery + .split(',') + .map(Number) + .filter((id) => !isNaN(id)); +}; + +export const getInstrumentFilterIds = ( + instrumentFilter: InstrumentFilterInput | null | undefined +): number[] | undefined => { + if (instrumentFilter?.instrumentIds) { + return instrumentFilter.instrumentIds; + } + + return undefined; +}; + type InstrumentFilterProps = { instruments?: InstrumentMinimalFragment[]; isLoading?: boolean; @@ -26,13 +55,13 @@ type InstrumentFilterProps = { shouldShowAll?: boolean; shouldShowMultiple?: boolean; showMultiInstrumentProposals?: boolean; - instrumentId?: number | null; + instrumentIds?: (number | null)[]; }; const InstrumentFilter = ({ instruments, isLoading, - instrumentId, + instrumentIds, onChange, shouldShowAll, shouldShowMultiple, @@ -49,6 +78,108 @@ const InstrumentFilter = ({ * NOTE: We might use https://material-ui.com/components/autocomplete/. * If we have lot of dropdown options to be able to search. */ + // Determine the current selected values for the Select component. + // Array of instruments unless 'ALL' or 'MULTI' is selected. + let currentValue: string[]; + if (showMultiInstrumentProposals) { + currentValue = [InstrumentFilterEnum.MULTI]; + } else if (instrumentIds && instrumentIds.length > 0) { + const validIds = instrumentIds.filter((id): id is number => id != null); + currentValue = validIds.map(String); + } else { + currentValue = [InstrumentFilterEnum.ALL]; + } + + const handleChange = (event: SelectChangeEvent) => { + const rawValue = event.target.value; + const selected = + typeof rawValue === 'string' ? rawValue.split(',') : rawValue; + + // Check if 'ALL' or 'MULTI' was selected + const lastSelected = selected[selected.length - 1]; + if ( + lastSelected === InstrumentFilterEnum.ALL || + lastSelected === InstrumentFilterEnum.MULTI + ) { + // Clear instrument selections + const newValue: InstrumentFilterInput = { + instrumentIds: null, + showMultiInstrumentProposals: + lastSelected === InstrumentFilterEnum.MULTI, + showAllProposals: lastSelected === InstrumentFilterEnum.ALL, + }; + setSearchParams((searchParams) => { + searchParams.delete('instrument'); + if (lastSelected === InstrumentFilterEnum.MULTI) { + searchParams.set('instrument', InstrumentFilterEnum.MULTI); + } + + return searchParams; + }); + onChange?.(newValue); + + return; + } + + const instrumentIdValues = selected + .filter( + (val) => + val !== InstrumentFilterEnum.ALL && val !== InstrumentFilterEnum.MULTI + ) + .map(Number) + .filter((id) => !isNaN(id)); + + if (instrumentIdValues.length === 0) { + const newValue: InstrumentFilterInput = { + instrumentIds: null, + showMultiInstrumentProposals: false, + showAllProposals: true, + }; + setSearchParams((searchParams) => { + searchParams.delete('instrument'); + + return searchParams; + }); + onChange?.(newValue); + + return; + } + + const newValue: InstrumentFilterInput = { + instrumentIds: instrumentIdValues, + showMultiInstrumentProposals: false, + showAllProposals: false, + }; + + setSearchParams((searchParams) => { + searchParams.set('instrument', instrumentIdValues.join(',')); + + return searchParams; + }); + onChange?.(newValue); + }; + + const renderValue = (selected: string[]) => { + if (selected.includes(InstrumentFilterEnum.ALL) || selected.length === 0) { + return 'All'; + } + if (selected.includes(InstrumentFilterEnum.MULTI)) { + return 'Multiple'; + } + + const selectedNames: string[] = []; + for (const id of selected) { + const matchingInstrument = instruments.find( + (instrument) => instrument.id === Number(id) + ); + if (matchingInstrument) { + selectedNames.push(matchingInstrument.name); + } + } + + return selectedNames.join(', '); + }; + return ( <> @@ -61,43 +192,10 @@ const InstrumentFilter = ({ diff --git a/apps/frontend/src/components/experiment/ExperimentFilterBar.tsx b/apps/frontend/src/components/experiment/ExperimentFilterBar.tsx index fc23566409..fdf651672c 100644 --- a/apps/frontend/src/components/experiment/ExperimentFilterBar.tsx +++ b/apps/frontend/src/components/experiment/ExperimentFilterBar.tsx @@ -91,14 +91,18 @@ function ExperimentFilterBar({ { + const [selectedInstrumentId] = + instrumentFilterValue.instrumentIds ?? []; setExperimentFilter({ ...filter, - instrumentId: instrumentFilterValue.instrumentId, + instrumentId: selectedInstrumentId, }); }} /> diff --git a/apps/frontend/src/components/fap/Proposals/FapProposalsAndAssignmentsView.tsx b/apps/frontend/src/components/fap/Proposals/FapProposalsAndAssignmentsView.tsx index 5ab7de7258..cf8c4909d0 100644 --- a/apps/frontend/src/components/fap/Proposals/FapProposalsAndAssignmentsView.tsx +++ b/apps/frontend/src/components/fap/Proposals/FapProposalsAndAssignmentsView.tsx @@ -3,7 +3,9 @@ import React, { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import CallFilter from 'components/common/proposalFilters/CallFilter'; -import InstrumentFilter from 'components/common/proposalFilters/InstrumentFilter'; +import InstrumentFilter, { + parseInstrumentQuery, +} from 'components/common/proposalFilters/InstrumentFilter'; import { Fap } from 'generated/sdk'; import { useCallsData } from 'hooks/call/useCallsData'; import { FapProposals } from 'hooks/fap/useFapProposalsData'; @@ -66,7 +68,7 @@ const FapProposalsAndAssignments = ({ instruments={instruments} isLoading={loadingInstruments} shouldShowAll={true} - instrumentId={instrument ? +instrument : null} + instrumentIds={parseInstrumentQuery(instrument) ?? undefined} data-cy="instrument-filter" /> diff --git a/apps/frontend/src/components/fap/Proposals/LegacyFapProposals.tsx b/apps/frontend/src/components/fap/Proposals/LegacyFapProposals.tsx index 561992fe3c..9d63c6f85d 100644 --- a/apps/frontend/src/components/fap/Proposals/LegacyFapProposals.tsx +++ b/apps/frontend/src/components/fap/Proposals/LegacyFapProposals.tsx @@ -3,7 +3,9 @@ import React, { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import CallFilter from 'components/common/proposalFilters/CallFilter'; -import InstrumentFilter from 'components/common/proposalFilters/InstrumentFilter'; +import InstrumentFilter, { + parseInstrumentQuery, +} from 'components/common/proposalFilters/InstrumentFilter'; import { Fap } from 'generated/sdk'; import { useCallsData } from 'hooks/call/useCallsData'; import { useFapProposalsData } from 'hooks/fap/useFapProposalsData'; @@ -72,7 +74,7 @@ const LegacyFapProposals = ({ instruments={instruments} isLoading={loadingInstruments} shouldShowAll={true} - instrumentId={instrument ? +instrument : null} + instrumentIds={parseInstrumentQuery(instrument) ?? undefined} data-cy="instrument-filter" /> diff --git a/apps/frontend/src/components/proposal/ProposalFilterBar.tsx b/apps/frontend/src/components/proposal/ProposalFilterBar.tsx index 8a57a9e811..a87e776dd2 100644 --- a/apps/frontend/src/components/proposal/ProposalFilterBar.tsx +++ b/apps/frontend/src/components/proposal/ProposalFilterBar.tsx @@ -2,7 +2,9 @@ import Grid from '@mui/material/Grid'; import React from 'react'; import CallFilter from 'components/common/proposalFilters/CallFilter'; -import InstrumentFilter from 'components/common/proposalFilters/InstrumentFilter'; +import InstrumentFilter, { + getInstrumentFilterIds, +} from 'components/common/proposalFilters/InstrumentFilter'; import QuestionaryFilter from 'components/common/proposalFilters/QuestionaryFilter'; import ProposalStatusFilter from 'components/common/proposalFilters/StatusFilter'; import { @@ -82,7 +84,7 @@ const ProposalFilterBar = ({ ({ callId: callId ? +callId : undefined, instrumentFilter: { - instrumentId: instrumentId != null ? +instrumentId : null, + instrumentIds: parseInstrumentQuery(instrumentId), showAllProposals: !instrumentId, - showMultiInstrumentProposals: false, + showMultiInstrumentProposals: instrumentId === 'multi', }, proposalStatusId: proposalStatusId ?? undefined, referenceNumbers: proposalId ? [proposalId] : undefined, diff --git a/apps/frontend/src/components/proposal/ProposalTableInstrumentScientist.tsx b/apps/frontend/src/components/proposal/ProposalTableInstrumentScientist.tsx index 905ce98f07..542651641a 100644 --- a/apps/frontend/src/components/proposal/ProposalTableInstrumentScientist.tsx +++ b/apps/frontend/src/components/proposal/ProposalTableInstrumentScientist.tsx @@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import MaterialTable from 'components/common/DenseMaterialTable'; +import { parseInstrumentQuery } from 'components/common/proposalFilters/InstrumentFilter'; import ProposalReviewContent, { PROPOSAL_MODAL_TAB_NAMES, } from 'components/review/ProposalReviewContent'; @@ -308,9 +309,9 @@ const ProposalTableInstrumentScientist = ({ const [proposalFilter, setProposalFilter] = useState({ callId: callId ? +callId : undefined, instrumentFilter: { - instrumentId: instrumentId != null ? +instrumentId : null, + instrumentIds: parseInstrumentQuery(instrumentId), showAllProposals: !instrumentId, - showMultiInstrumentProposals: false, + showMultiInstrumentProposals: instrumentId === 'multi', }, proposalStatusId: proposalStatusId, referenceNumbers: proposalId ? [proposalId] : undefined, diff --git a/apps/frontend/src/components/review/ProposalTableReviewer.tsx b/apps/frontend/src/components/review/ProposalTableReviewer.tsx index 83f298279a..5fe4898461 100644 --- a/apps/frontend/src/components/review/ProposalTableReviewer.tsx +++ b/apps/frontend/src/components/review/ProposalTableReviewer.tsx @@ -14,7 +14,9 @@ import { useSearchParams } from 'react-router-dom'; import MaterialTable from 'components/common/DenseMaterialTable'; import CallFilter from 'components/common/proposalFilters/CallFilter'; -import InstrumentFilter from 'components/common/proposalFilters/InstrumentFilter'; +import InstrumentFilter, { + parseInstrumentQuery, +} from 'components/common/proposalFilters/InstrumentFilter'; import { UserContext } from 'context/UserContextProvider'; import { PaginationSortDirection, @@ -107,14 +109,15 @@ const ProposalTableReviewer = ({ confirm }: { confirm: WithConfirmType }) => { const [selectedCallId, setSelectedCallId] = useState( call ? +call : 0 ); - const [selectedInstrumentId, setSelectedInstrumentId] = useState< - number | null | undefined - >(instrument ? +instrument : null); + const [selectedInstrumentIds, setSelectedInstrumentIds] = useState( + parseInstrumentQuery(instrument) ?? [] + ); const { loading, userData, setUserData, setUserWithReviewsFilter } = useUserWithReviewsData({ callId: selectedCallId, - instrumentId: selectedInstrumentId, + instrumentId: + selectedInstrumentIds.length > 0 ? selectedInstrumentIds[0] : null, status: getFilterStatus(reviewStatus), reviewer: getFilterReviewer(reviewer), active: true, @@ -461,12 +464,15 @@ const ProposalTableReviewer = ({ confirm }: { confirm: WithConfirmType }) => { shouldShowAll instruments={instruments} isLoading={loadingInstruments} - instrumentId={selectedInstrumentId} + instrumentIds={selectedInstrumentIds} onChange={(instrumentFilter) => { - setSelectedInstrumentId(instrumentFilter.instrumentId); - setUserWithReviewsFilter((filters) => ({ - ...filters, - instrumentId: instrumentFilter.instrumentId, + const selectedIds = instrumentFilter.instrumentIds ?? []; + setSelectedInstrumentIds(selectedIds); + const firstSelectedId = + selectedIds.length > 0 ? selectedIds[0] : null; + setUserWithReviewsFilter((currentFilters) => ({ + ...currentFilters, + instrumentId: firstSelectedId, })); }} /> diff --git a/apps/frontend/src/components/techniqueProposal/TechniqueProposalFilterBar.tsx b/apps/frontend/src/components/techniqueProposal/TechniqueProposalFilterBar.tsx index 4801fd2f9e..2e85c70e2a 100644 --- a/apps/frontend/src/components/techniqueProposal/TechniqueProposalFilterBar.tsx +++ b/apps/frontend/src/components/techniqueProposal/TechniqueProposalFilterBar.tsx @@ -3,7 +3,9 @@ import React from 'react'; import CallFilter from 'components/common/proposalFilters/CallFilter'; import DateFilter from 'components/common/proposalFilters/DateFilter'; -import InstrumentFilter from 'components/common/proposalFilters/InstrumentFilter'; +import InstrumentFilter, { + getInstrumentFilterIds, +} from 'components/common/proposalFilters/InstrumentFilter'; import ProposalStatusFilter from 'components/common/proposalFilters/StatusFilter'; import TechniqueFilter from 'components/common/proposalFilters/TechniqueFilter'; import { @@ -68,7 +70,7 @@ const TechniqueProposalFilterBar = ({ { const [proposalFilter, setProposalFilter] = useState({ callId, instrumentFilter: { - instrumentId: instrument ? +instrument : null, + instrumentIds: parseInstrumentQuery(instrument), showAllProposals: !instrument, - showMultiInstrumentProposals: false, + showMultiInstrumentProposals: instrument === 'multi', }, techniqueFilter: { techniqueId: technique ? +technique : null,