From 7b5facb2539032d465b8e381983f65c1d2699717 Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Mon, 1 Jun 2026 09:05:57 +0100 Subject: [PATCH 01/12] adds filtering on tags for user officer derived roles --- .../src/datasources/InstrumentDataSource.ts | 3 +- .../mockups/InstrumentDataSource.ts | 3 +- .../postgres/InstrumentDataSource.ts | 64 ++++++++++++------- .../postgres/ProposalDataSource.ts | 17 +++-- apps/backend/src/queries/InstrumentQueries.ts | 3 +- .../src/resolvers/queries/ProposalsQuery.ts | 3 + .../TechniqueProposalTable.tsx | 13 ++++ 7 files changed, 75 insertions(+), 31 deletions(-) diff --git a/apps/backend/src/datasources/InstrumentDataSource.ts b/apps/backend/src/datasources/InstrumentDataSource.ts index afed63a28b..efcf413506 100644 --- a/apps/backend/src/datasources/InstrumentDataSource.ts +++ b/apps/backend/src/datasources/InstrumentDataSource.ts @@ -15,7 +15,8 @@ export interface InstrumentDataSource { getInstrumentsByIds(instrumentIds: number[]): Promise; getInstruments( first?: number, - offset?: number + offset?: number, + agentId?: number ): Promise<{ totalCount: number; instruments: Instrument[] }>; getUserInstruments(userId: number, agentId?: number): Promise; getInstrumentsByCallId( diff --git a/apps/backend/src/datasources/mockups/InstrumentDataSource.ts b/apps/backend/src/datasources/mockups/InstrumentDataSource.ts index 13b9f40276..52ced2b3ac 100644 --- a/apps/backend/src/datasources/mockups/InstrumentDataSource.ts +++ b/apps/backend/src/datasources/mockups/InstrumentDataSource.ts @@ -129,7 +129,8 @@ export class InstrumentDataSourceMock implements InstrumentDataSource { async getInstruments( first?: number, - offset?: number + offset?: number, + agentId?: number ): Promise<{ totalCount: number; instruments: Instrument[] }> { return { totalCount: 1, instruments: [dummyInstrument] }; } diff --git a/apps/backend/src/datasources/postgres/InstrumentDataSource.ts b/apps/backend/src/datasources/postgres/InstrumentDataSource.ts index 6cf9e1f56b..828e4c6321 100644 --- a/apps/backend/src/datasources/postgres/InstrumentDataSource.ts +++ b/apps/backend/src/datasources/postgres/InstrumentDataSource.ts @@ -133,31 +133,51 @@ export default class PostgresInstrumentDataSource async getInstruments( first?: number, - offset?: number + offset?: number, + agentRoleId?: number ): Promise<{ totalCount: number; instruments: Instrument[] }> { - return database - .select(['*', database.raw('count(*) OVER() AS full_count')]) - .from('instruments') - .orderBy('instrument_id', 'desc') - .modify((query) => { - if (first) { - query.limit(first); - } - if (offset) { - query.offset(offset); - } - }) - .then((instruments: InstrumentRecord[]) => { - const result = instruments.map((instrument) => - this.createInstrumentObject(instrument) - ); + let instruments: InstrumentRecord[]; + const tags = agentRoleId ? (await this.getTagsByRoleId(agentRoleId)) ?? [] : []; + if (tags.length > 0) { + const tagIds = tags.map((tag) => tag.id); - return { - totalCount: instruments[0] ? instruments[0].full_count : 0, - instruments: result, - }; - }); + instruments = await database + .select(['i.*', database.raw('count(*) OVER() AS full_count')]) + .from('instruments as i') + .join('tag_instrument as ti', 'i.instrument_id', 'ti.instrument_id') + .whereIn('ti.tag_id', tagIds) + .orderBy('i.instrument_id', 'desc') + .modify((query) => { + if (first) { + query.limit(first); + } + if (offset) { + query.offset(offset); + } + }) + } else { + instruments = await database + .select(['*', database.raw('count(*) OVER() AS full_count')]) + .from('instruments') + .orderBy('instrument_id', 'desc') + .modify((query) => { + if (first) { + query.limit(first); + } + if (offset) { + query.offset(offset); + } + }) + } + const result = instruments.map((instrument) => + this.createInstrumentObject(instrument) + ); + return { + totalCount: instruments[0] ? instruments[0].full_count : 0, + instruments: result, + }; } + async getTagsByRoleId(roleId: number): Promise { try { const rows = await database diff --git a/apps/backend/src/datasources/postgres/ProposalDataSource.ts b/apps/backend/src/datasources/postgres/ProposalDataSource.ts index f81d377850..7631b419c2 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -1218,15 +1218,20 @@ export default class PostgresProposalDataSource implements ProposalDataSource { 'ins.instrument_id' ) .modify((query) => { - const instrumentId = filter?.instrumentFilter?.instrumentId; + const instrumentIdsToFilter = Array.from( + new Set([ + ...(filter?.instrumentIds || []), + ...(filter?.instrumentId ? [filter.instrumentId] : []), + ]) + ); + + if (instrumentIdsToFilter.length > 0) { - if (instrumentId && !isNaN(instrumentId)) { 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 - ); + instrumentIdsToFilter + ) }); } }) diff --git a/apps/backend/src/queries/InstrumentQueries.ts b/apps/backend/src/queries/InstrumentQueries.ts index c5261d12a6..d11f25b122 100644 --- a/apps/backend/src/queries/InstrumentQueries.ts +++ b/apps/backend/src/queries/InstrumentQueries.ts @@ -40,8 +40,9 @@ export default class InstrumentQueries { @Authorized([Roles.USER_OFFICER, Roles.INSTRUMENT_SCIENTIST]) async getAll(agent: UserWithRole | null, callIds: number[]) { + const agentRoleId = this.userAuth.isApiToken(agent) ? undefined : agent?.currentRole?.id; if (!callIds || callIds.length === 0) { - return await this.dataSource.getInstruments(); + return await this.dataSource.getInstruments(undefined, undefined, agentRoleId); } else { const instrumentsByCallIds = await this.dataSource.getInstrumentsByCallId(callIds); diff --git a/apps/backend/src/resolvers/queries/ProposalsQuery.ts b/apps/backend/src/resolvers/queries/ProposalsQuery.ts index 1d115ca795..0bc968fb59 100644 --- a/apps/backend/src/resolvers/queries/ProposalsQuery.ts +++ b/apps/backend/src/resolvers/queries/ProposalsQuery.ts @@ -84,6 +84,9 @@ export class ProposalsFilter { @Field(() => Int, { nullable: true }) public instrumentId?: number; + @Field(() => [Int], { nullable: true }) + public instrumentIds?: number[]; + @Field(() => InstrumentFilterInput, { nullable: true }) public instrumentFilter?: InstrumentFilterInput; diff --git a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx index 04cd8fe591..7ff8135f8c 100644 --- a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx +++ b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx @@ -701,6 +701,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { callId, callIds, instrumentFilter, + instrumentIds, techniqueFilter, proposalStatusId, text, @@ -719,6 +720,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { callId, callIds, instrumentFilter, + instrumentIds: [23], techniqueFilter, proposalStatusId, text, @@ -834,6 +836,15 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { } } + if (filter.instrumentId != null) { + if (filter.instrumentId === 0) { + updatedFilter.instrumentId = null; + updatedFilter.instrumentIds = allInstruments?.map((instrument) => instrument.id) || []; + } else { + updatedFilter.instrumentIds = [filter.instrumentId as number]; + } + } + setProposalFilter(updatedFilter); refreshTableData(); }; @@ -857,6 +868,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { callId, callIds, instrumentFilter, + instrumentIds = [23], techniqueFilter, proposalStatusId, text, @@ -874,6 +886,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { .getTechniqueScientistProposalsBasic({ filter: { callId, + instrumentIds, instrumentFilter, callIds, techniqueFilter, From f659a489776a5beae8877f83e6135b2c9bf03f87 Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Wed, 10 Jun 2026 12:12:14 +0100 Subject: [PATCH 02/12] technique proposals instrument filter only show options visible to derived role, lint changes --- .../postgres/InstrumentDataSource.ts | 9 +- .../postgres/ProposalDataSource.ts | 3 +- apps/backend/src/queries/InstrumentQueries.ts | 10 +- .../TechniqueProposalTable.tsx | 102 +++++++++++------- 4 files changed, 79 insertions(+), 45 deletions(-) diff --git a/apps/backend/src/datasources/postgres/InstrumentDataSource.ts b/apps/backend/src/datasources/postgres/InstrumentDataSource.ts index 828e4c6321..357f91b981 100644 --- a/apps/backend/src/datasources/postgres/InstrumentDataSource.ts +++ b/apps/backend/src/datasources/postgres/InstrumentDataSource.ts @@ -137,7 +137,9 @@ export default class PostgresInstrumentDataSource agentRoleId?: number ): Promise<{ totalCount: number; instruments: Instrument[] }> { let instruments: InstrumentRecord[]; - const tags = agentRoleId ? (await this.getTagsByRoleId(agentRoleId)) ?? [] : []; + const tags = agentRoleId + ? (await this.getTagsByRoleId(agentRoleId)) ?? [] + : []; if (tags.length > 0) { const tagIds = tags.map((tag) => tag.id); @@ -154,7 +156,7 @@ export default class PostgresInstrumentDataSource if (offset) { query.offset(offset); } - }) + }); } else { instruments = await database .select(['*', database.raw('count(*) OVER() AS full_count')]) @@ -167,11 +169,12 @@ export default class PostgresInstrumentDataSource if (offset) { query.offset(offset); } - }) + }); } const result = instruments.map((instrument) => this.createInstrumentObject(instrument) ); + return { totalCount: instruments[0] ? instruments[0].full_count : 0, instruments: result, diff --git a/apps/backend/src/datasources/postgres/ProposalDataSource.ts b/apps/backend/src/datasources/postgres/ProposalDataSource.ts index 7631b419c2..d0e32dea21 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -1226,12 +1226,11 @@ export default class PostgresProposalDataSource implements ProposalDataSource { ); if (instrumentIdsToFilter.length > 0) { - query.join('instrument_has_proposals as ihp', function () { this.on('ihp.proposal_pk', '=', 'proposals.proposal_pk').andOnIn( 'ihp.instrument_id', instrumentIdsToFilter - ) + ); }); } }) diff --git a/apps/backend/src/queries/InstrumentQueries.ts b/apps/backend/src/queries/InstrumentQueries.ts index d11f25b122..bb0f45df24 100644 --- a/apps/backend/src/queries/InstrumentQueries.ts +++ b/apps/backend/src/queries/InstrumentQueries.ts @@ -40,9 +40,15 @@ export default class InstrumentQueries { @Authorized([Roles.USER_OFFICER, Roles.INSTRUMENT_SCIENTIST]) async getAll(agent: UserWithRole | null, callIds: number[]) { - const agentRoleId = this.userAuth.isApiToken(agent) ? undefined : agent?.currentRole?.id; + const agentRoleId = this.userAuth.isApiToken(agent) + ? undefined + : agent?.currentRole?.id; if (!callIds || callIds.length === 0) { - return await this.dataSource.getInstruments(undefined, undefined, agentRoleId); + return await this.dataSource.getInstruments( + undefined, + undefined, + agentRoleId + ); } else { const instrumentsByCallIds = await this.dataSource.getInstrumentsByCallId(callIds); diff --git a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx index 7ff8135f8c..773dcbe9b1 100644 --- a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx +++ b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx @@ -47,6 +47,7 @@ import { import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; import { CallsDataQuantity, useCallsData } from 'hooks/call/useCallsData'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; +import { useInstrumentsMinimalData } from 'hooks/instrument/useInstrumentsMinimalData'; import { useDownloadXLSXProposal } from 'hooks/proposal/useDownloadXLSXProposal'; import { ProposalViewData } from 'hooks/proposal/useProposalsCoreData'; import { useStatusesData } from 'hooks/settings/useStatusesData'; @@ -83,11 +84,27 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { const { techniques, loadingTechniques } = useTechniqueProposalsTechniquesData(); + const { + instruments: instrumentsMinimal, + loadingInstruments: loadingInstrumentsMinimal, + } = useInstrumentsMinimalData(); + const instrumentIds = useMemo(() => { - if (loadingTechniques || !techniques) return []; + if (currentRole === UserRole.USER_OFFICER) { + if (loadingInstrumentsMinimal || !instrumentsMinimal) return []; - return techniques.flatMap((t) => t.instruments.map((i) => i.id)); - }, [loadingTechniques, techniques]); + return instrumentsMinimal.map((i) => i.id); + } else { + if (loadingTechniques || !techniques) return []; + + return techniques.flatMap((t) => t.instruments.map((i) => i.id)); + } + }, [ + loadingTechniques, + techniques, + loadingInstrumentsMinimal, + instrumentsMinimal, + ]); const { calls, loadingCalls, setCallsFilter } = useCallsData( { @@ -193,6 +210,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { const [proposalFilter, setProposalFilter] = useState({ callId, + instrumentIds, instrumentFilter: { instrumentId: instrument ? +instrument : null, showAllProposals: !instrument, @@ -227,7 +245,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { return prev; }); - setProposalFilter((prev) => ({ ...prev, callId })); + setProposalFilter((prev) => ({ ...prev, callId, instrumentIds })); refreshTableData(); } }, [callId, setSearchParams, setProposalFilter, refreshTableData]); @@ -713,32 +731,40 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { proposals: ProposalViewData[] | undefined; totalCount: number; } = { proposals: undefined, totalCount: 0 }; - api() - .getTechniqueScientistProposals({ - filter: { - callId, - callIds, - instrumentFilter, - instrumentIds: [23], - techniqueFilter, - proposalStatusId, - text, - referenceNumbers, - dateFilter, - excludeProposalStatusIds: - currentRole === UserRole.INSTRUMENT_SCIENTIST ? [9] : [], // Hide expired from scientists - }, - sortField: orderBy?.orderByField, - 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, + .getInstrumentsMinimal() + .then((data) => { + return data.instruments?.instruments.map((i) => i.id); + }) + .then((fetchedInstrumentIds) => { + return api().getTechniqueScientistProposals({ + filter: { + callId, + callIds, + instrumentFilter, + instrumentIds: + instrumentIds != null && instrumentIds.length > 0 + ? instrumentIds + : fetchedInstrumentIds, + techniqueFilter, + proposalStatusId, + text, + referenceNumbers, + dateFilter, + excludeProposalStatusIds: + currentRole === UserRole.INSTRUMENT_SCIENTIST ? [9] : [], // Hide expired from scientists + }, + sortField: orderBy?.orderByField, + 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, + }); }) .then((data) => { result.totalCount = @@ -836,15 +862,15 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { } } - if (filter.instrumentId != null) { - if (filter.instrumentId === 0) { - updatedFilter.instrumentId = null; - updatedFilter.instrumentIds = allInstruments?.map((instrument) => instrument.id) || []; - } else { - updatedFilter.instrumentIds = [filter.instrumentId as number]; - } + if (filter.instrumentFilter?.instrumentId != null) { + updatedFilter.instrumentIds = [ + filter.instrumentFilter.instrumentId as number, + ]; + } else { + updatedFilter.instrumentId = null; + updatedFilter.instrumentIds = + allInstruments?.map((instrument) => instrument.id) || []; } - setProposalFilter(updatedFilter); refreshTableData(); }; @@ -868,7 +894,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { callId, callIds, instrumentFilter, - instrumentIds = [23], + instrumentIds, techniqueFilter, proposalStatusId, text, From 01aaa1509c46fcc26740c5dc542cd3019219e4f1 Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Wed, 10 Jun 2026 14:32:38 +0100 Subject: [PATCH 03/12] only relevant techniques are shown in filter for derived user officer roles --- .../techniqueProposal/TechniqueProposalTable.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx index 773dcbe9b1..fcd8e5ce90 100644 --- a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx +++ b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx @@ -106,6 +106,15 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { instrumentsMinimal, ]); + const relevantTechniques = + currentRole === UserRole.USER_OFFICER + ? techniques.filter((t) => + t.instruments + .map((ti) => ti.id) + .some((i) => instrumentsMinimal.map((im) => im.id).includes(i)) + ) + : techniques; + const { calls, loadingCalls, setCallsFilter } = useCallsData( { proposalStatusShortCode: 'QUICK_REVIEW', @@ -990,7 +999,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { isLoading: loadingInstruments, }} techniques={{ - data: techniques, + data: relevantTechniques, isLoading: loadingTechniques, }} proposalStatuses={{ From a47d107fc639f6d2a38e6314f867a2a20da62a2b Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Wed, 17 Jun 2026 09:34:02 +0100 Subject: [PATCH 04/12] populate default role config object --- .../datasources/postgres/RoleDataSource.ts | 2 +- .../src/components/admin/RoleModal.tsx | 2 +- .../settings/workflow/WorkflowEditorModel.tsx | 1 - .../TechniqueProposalTable.tsx | 50 +++++++++---------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/apps/backend/src/datasources/postgres/RoleDataSource.ts b/apps/backend/src/datasources/postgres/RoleDataSource.ts index 801fe8ce3d..11a68c2364 100644 --- a/apps/backend/src/datasources/postgres/RoleDataSource.ts +++ b/apps/backend/src/datasources/postgres/RoleDataSource.ts @@ -23,7 +23,7 @@ function defaultConfig(shortCode: string): unknown { hasAdminAccess: false, }; default: - return {}; + return { note: '' }; } } diff --git a/apps/frontend/src/components/admin/RoleModal.tsx b/apps/frontend/src/components/admin/RoleModal.tsx index 25cf39f200..d694a12d36 100644 --- a/apps/frontend/src/components/admin/RoleModal.tsx +++ b/apps/frontend/src/components/admin/RoleModal.tsx @@ -127,7 +127,7 @@ const RoleModal: React.FC = ({ config: { proposalReader: config as ProposalReaderRoleConfig }, }; default: - return { config: {} }; + return { config: { user: { note: '' } } }; } }; diff --git a/apps/frontend/src/components/settings/workflow/WorkflowEditorModel.tsx b/apps/frontend/src/components/settings/workflow/WorkflowEditorModel.tsx index 7e767844b4..ded042c8a5 100644 --- a/apps/frontend/src/components/settings/workflow/WorkflowEditorModel.tsx +++ b/apps/frontend/src/components/settings/workflow/WorkflowEditorModel.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { produce } from 'immer'; import { Reducer, useCallback, useEffect } from 'react'; import { useParams } from 'react-router-dom'; diff --git a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx index df99184cc6..79c3fe9140 100644 --- a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx +++ b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx @@ -758,34 +758,34 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { }) .then((fetchedInstrumentIds) => { return api().getTechniqueScientistProposals({ - filter: { - callId, - callIds, - instrumentFilter, - instrumentIds: + filter: { + callId, + callIds, + instrumentFilter, + instrumentIds: instrumentIds != null && instrumentIds.length > 0 ? instrumentIds : fetchedInstrumentIds, - techniqueFilter, - proposalStatusId, - text, - referenceNumbers, - dateFilter, - excludeProposalStatusIds: - currentRole === UserRole.INSTRUMENT_SCIENTIST - ? [StatusCode.EXPIRED] - : [], - }, - sortField: orderBy?.orderByField, - 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, + techniqueFilter, + proposalStatusId, + text, + referenceNumbers, + dateFilter, + excludeProposalStatusIds: + currentRole === UserRole.INSTRUMENT_SCIENTIST + ? [StatusCode.EXPIRED] + : [], + }, + sortField: orderBy?.orderByField, + 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, }); }) .then((data) => { From 6fe064c4876e49430734a00a5407c7aa14511c7a Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Mon, 22 Jun 2026 15:43:10 +0100 Subject: [PATCH 05/12] adds create role type definition for e2e --- apps/e2e/cypress/types/role.d.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 apps/e2e/cypress/types/role.d.ts diff --git a/apps/e2e/cypress/types/role.d.ts b/apps/e2e/cypress/types/role.d.ts new file mode 100644 index 0000000000..bf6245b0f3 --- /dev/null +++ b/apps/e2e/cypress/types/role.d.ts @@ -0,0 +1,22 @@ +import { + CreateRoleMutationVariables, + CreateRoleMutation, +} from '@user-office-software-libs/shared-types'; + +declare global { + namespace Cypress { + interface Chainable { + /** + * Creates new role. + * + * @returns {typeof createRole} + * @memberof Chainable + * @example + * cy.createRole(createRoleInput: CreateRoleMutationVariables) + */ + createRole: ( + createRoleInput: CreateRoleMutationVariables + ) => Cypress.Chainable; + } + } +} From 5cbbaf1a49351d461fa84fc7f460b53c162483f0 Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Wed, 24 Jun 2026 10:00:06 +0100 Subject: [PATCH 06/12] adds e2e tests --- apps/e2e/cypress/e2e/techniqueProposals.cy.ts | 214 ++++++++++++++++++ apps/e2e/cypress/support/tag.ts | 12 + 2 files changed, 226 insertions(+) diff --git a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts index 8ffeff06ca..793d30e458 100644 --- a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts +++ b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts @@ -241,6 +241,8 @@ context('Technique Proposal tests', () => { abstract: faker.word.words(5), }; + let derivedUserOfficerRoleId: number; + beforeEach(function () { cy.resetDB(); @@ -576,6 +578,34 @@ context('Technique Proposal tests', () => { }); } }); + + cy.createTag({ name: 'Tag', shortCode: 'tag' }).then((tagResult) => { + cy.assignCallsToTag({ + callIds: initialDBData.call.id, + tagId: tagResult.createTag.id, + }); + cy.addInstrumentToTag({ + instrumentIds: createdInstrumentId1, + tagId: tagResult.createTag.id, + }); + cy.createRole({ + args: { + title: 'Derived User Officer', + shortCode: 'user_officer', + description: '', + }, + }).then((roleResult) => { + derivedUserOfficerRoleId = roleResult.createRole.id; + cy.updateRoleTags({ + roleId: roleResult.createRole.id, + tagIds: tagResult.createTag.id, + }); + cy.updateUserRoles({ + id: initialDBData.users.officer.id, + roles: [roleResult.createRole.id], + }); + }); + }); }); describe('Technique proposal basic tests', () => { @@ -2042,4 +2072,188 @@ context('Technique Proposal tests', () => { }); }); }); + + describe('Technique proposal tags tests', () => { + beforeEach(function () { + cy.getAndStoreFeaturesEnabled().then(() => { + if ( + !featureFlags.getEnabledFeatures().get(FeatureId.TECHNIQUE_PROPOSALS) + ) { + this.skip(); + } + }); + }); + + it('Base User Officer role can see all call filter options', () => { + cy.createCall({ + shortCode: 'untagged call', + cycleComment: 'This is cycle comment', + allocationTimeUnit: AllocationTimeUnits.DAY, + endCall: DateTime.fromJSDate(faker.date.future()), + endCycle: DateTime.fromJSDate(faker.date.future()), + endNotify: DateTime.fromJSDate(faker.date.future()), + endReview: DateTime.fromJSDate(faker.date.future()), + proposalWorkflowId: initialDBData.workflows.defaultWorkflow.id, + startCall: DateTime.fromJSDate(faker.date.past()), + startCycle: DateTime.fromJSDate(faker.date.past()), + startNotify: DateTime.fromJSDate(faker.date.past()), + startReview: DateTime.fromJSDate(faker.date.past()), + templateId: initialDBData.template.id, + }).then((result) => + cy.updateCall({ + id: result.createCall.id, + proposalWorkflowId: callWorkflowId, + }) + ); + + cy.login('officer'); + cy.changeActiveRole(initialDBData.roles.userOfficer); + cy.visit('/'); + cy.finishedLoading(); + + cy.contains('Xpress Proposals').click(); + + cy.get('[data-cy="call-filter"]').click(); + cy.get('[role="listbox"]').contains('untagged call'); + cy.get('[role="listbox"]').contains('call 1'); + }); + + it('Base User Officer role can see all technique filter options', () => { + cy.login('officer'); + cy.changeActiveRole(initialDBData.roles.userOfficer); + cy.visit('/'); + cy.finishedLoading(); + + cy.contains('Xpress Proposals').click(); + + cy.get('[data-cy="technique-filter"]').click(); + cy.get('[role="listbox"]').contains(technique1.name); + cy.get('[role="listbox"]').contains(technique2.name); + cy.get('[role="listbox"]').contains(technique3.name); + cy.get('[role="listbox"]').contains(technique4.name); + cy.get('[role="listbox"]').contains(technique5.name); + }); + + it('Base User Officer role can see all instrument filter options', () => { + cy.login('officer'); + cy.changeActiveRole(initialDBData.roles.userOfficer); + cy.visit('/'); + cy.finishedLoading(); + + cy.contains('Xpress Proposals').click(); + + //the call dropdown will default to a call with no proposals + cy.get('[data-cy="call-filter"]').click(); + cy.get('[role="listbox"]').contains('All').click(); + cy.finishedLoading(); + + cy.get('[data-cy="instrument-filter"]').click(); + cy.get('[role="listbox"]').contains(instrument1.name); + cy.get('[role="listbox"]').contains(instrument2.name); + cy.get('[role="listbox"]').contains(instrument3.name); + cy.get('[role="listbox"]').contains(instrument4.name); + cy.get('[role="listbox"]').contains(instrument5.name); + }); + + it('Base User Officer role can see all proposals', () => { + cy.login('officer'); + cy.changeActiveRole(initialDBData.roles.userOfficer); + cy.visit('/'); + cy.finishedLoading(); + + cy.contains('Xpress Proposals').click(); + + //the call dropdown will default to a call with no proposals + cy.get('[data-cy="call-filter"]').click(); + cy.get('[role="listbox"]').contains('All').click(); + cy.finishedLoading(); + + cy.contains(createdProposalId1); + cy.contains(createdProposalId2); + cy.contains(createdProposalId3); + cy.contains(createdProposalId4); + cy.contains(createdProposalId5); + }); + + it('Derived User Officer role can only see appropriately tagged call filter options', () => { + cy.createCall({ + shortCode: 'untagged call', + cycleComment: 'This is cycle comment', + allocationTimeUnit: AllocationTimeUnits.DAY, + endCall: DateTime.fromJSDate(faker.date.future()), + endCycle: DateTime.fromJSDate(faker.date.future()), + endNotify: DateTime.fromJSDate(faker.date.future()), + endReview: DateTime.fromJSDate(faker.date.future()), + proposalWorkflowId: initialDBData.workflows.defaultWorkflow.id, + startCall: DateTime.fromJSDate(faker.date.past()), + startCycle: DateTime.fromJSDate(faker.date.past()), + startNotify: DateTime.fromJSDate(faker.date.past()), + startReview: DateTime.fromJSDate(faker.date.past()), + templateId: initialDBData.template.id, + }).then((result) => + cy.updateCall({ + id: result.createCall.id, + proposalWorkflowId: callWorkflowId, + }) + ); + + cy.login('officer'); + cy.changeActiveRole(derivedUserOfficerRoleId); + cy.visit('/'); + cy.finishedLoading(); + + cy.contains('Xpress Proposals').click(); + + cy.get('[data-cy="call-filter"]').click(); + cy.get('[role="listbox"]').contains('untagged call').should('not.exist'); + cy.get('[role="listbox"]').contains('call 1'); + }); + + it('Derived User Officer role can only see appropriate technique filter options', () => { + cy.login('officer'); + cy.changeActiveRole(derivedUserOfficerRoleId); + cy.visit('/'); + cy.finishedLoading(); + + cy.contains('Xpress Proposals').click(); + + cy.get('[data-cy="technique-filter"]').click(); + cy.get('[role="listbox"]').contains(technique1.name); + cy.get('[role="listbox"]').contains(technique2.name).should('not.exist'); + cy.get('[role="listbox"]').contains(technique3.name).should('not.exist'); + cy.get('[role="listbox"]').contains(technique4.name).should('not.exist'); + cy.get('[role="listbox"]').contains(technique5.name).should('not.exist'); + }); + + it('Derived User Officer role can only see appropriately tagged instrument filter options', () => { + cy.login('officer'); + cy.changeActiveRole(derivedUserOfficerRoleId); + cy.visit('/'); + cy.finishedLoading(); + + cy.contains('Xpress Proposals').click(); + + cy.get('[data-cy="instrument-filter"]').click(); + cy.get('[role="listbox"]').contains(instrument1.name); + cy.get('[role="listbox"]').contains(instrument2.name).should('not.exist'); + cy.get('[role="listbox"]').contains(instrument3.name).should('not.exist'); + cy.get('[role="listbox"]').contains(instrument4.name).should('not.exist'); + cy.get('[role="listbox"]').contains(instrument5.name).should('not.exist'); + }); + + it('Derived User Officer role can only see appropriate tagged proposals', () => { + cy.login('officer'); + cy.changeActiveRole(derivedUserOfficerRoleId); + cy.visit('/'); + cy.finishedLoading(); + + cy.contains('Xpress Proposals').click(); + + cy.contains(createdProposalId1); + cy.should('not.contain', createdProposalId2); + cy.should('not.contain', createdProposalId3); + cy.should('not.contain', createdProposalId4); + cy.should('not.contain', createdProposalId5); + }); + }); }); diff --git a/apps/e2e/cypress/support/tag.ts b/apps/e2e/cypress/support/tag.ts index 8a6c4c17cb..a0bed78e90 100644 --- a/apps/e2e/cypress/support/tag.ts +++ b/apps/e2e/cypress/support/tag.ts @@ -1,12 +1,14 @@ import { AssignCallsToTagMutation, AssignCallsToTagMutationVariables, + CreateRoleMutation, CreateTagMutation, CreateTagMutationVariables, AssignInstrumentsToTagMutationVariables, AssignInstrumentsToTagMutation, UpdateRoleTagsMutation, UpdateRoleTagsMutationVariables, + CreateRoleMutationVariables, } from '@user-office-software-libs/shared-types'; import { getE2EApi } from './utils'; @@ -20,6 +22,15 @@ const createTag = ( return cy.wrap(request); }; +const createRole = ( + createRoleInput: CreateRoleMutationVariables +): Cypress.Chainable => { + const api = getE2EApi(); + const request = api.createRole(createRoleInput); + + return cy.wrap(request); +}; + const addInstrumentToTag = ( addInstrumentsToTag: AssignInstrumentsToTagMutationVariables ): Cypress.Chainable => { @@ -48,6 +59,7 @@ const updateRoleTags = ( }; Cypress.Commands.add('createTag', createTag); +Cypress.Commands.add('createRole', createRole); Cypress.Commands.add('addInstrumentToTag', addInstrumentToTag); Cypress.Commands.add('assignCallsToTag', assignCallsToTag); Cypress.Commands.add('updateRoleTags', updateRoleTags); From 7a3aa933b1fb608d45610cdb235ed25ed0959b6b Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Wed, 24 Jun 2026 10:37:30 +0100 Subject: [PATCH 07/12] corrects element name in technique proposal e2e tests --- apps/e2e/cypress/e2e/techniqueProposals.cy.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts index 793d30e458..67b54c6e80 100644 --- a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts +++ b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts @@ -2111,7 +2111,7 @@ context('Technique Proposal tests', () => { cy.visit('/'); cy.finishedLoading(); - cy.contains('Xpress Proposals').click(); + cy.contains('Technique Proposals').click(); cy.get('[data-cy="call-filter"]').click(); cy.get('[role="listbox"]').contains('untagged call'); @@ -2124,7 +2124,7 @@ context('Technique Proposal tests', () => { cy.visit('/'); cy.finishedLoading(); - cy.contains('Xpress Proposals').click(); + cy.contains('Technique Proposals').click(); cy.get('[data-cy="technique-filter"]').click(); cy.get('[role="listbox"]').contains(technique1.name); @@ -2140,7 +2140,7 @@ context('Technique Proposal tests', () => { cy.visit('/'); cy.finishedLoading(); - cy.contains('Xpress Proposals').click(); + cy.contains('Technique Proposals').click(); //the call dropdown will default to a call with no proposals cy.get('[data-cy="call-filter"]').click(); @@ -2161,7 +2161,7 @@ context('Technique Proposal tests', () => { cy.visit('/'); cy.finishedLoading(); - cy.contains('Xpress Proposals').click(); + cy.contains('Technique Proposals').click(); //the call dropdown will default to a call with no proposals cy.get('[data-cy="call-filter"]').click(); @@ -2202,7 +2202,7 @@ context('Technique Proposal tests', () => { cy.visit('/'); cy.finishedLoading(); - cy.contains('Xpress Proposals').click(); + cy.contains('Technique Proposals').click(); cy.get('[data-cy="call-filter"]').click(); cy.get('[role="listbox"]').contains('untagged call').should('not.exist'); @@ -2215,7 +2215,7 @@ context('Technique Proposal tests', () => { cy.visit('/'); cy.finishedLoading(); - cy.contains('Xpress Proposals').click(); + cy.contains('Technique Proposals').click(); cy.get('[data-cy="technique-filter"]').click(); cy.get('[role="listbox"]').contains(technique1.name); @@ -2231,7 +2231,7 @@ context('Technique Proposal tests', () => { cy.visit('/'); cy.finishedLoading(); - cy.contains('Xpress Proposals').click(); + cy.contains('Technique Proposals').click(); cy.get('[data-cy="instrument-filter"]').click(); cy.get('[role="listbox"]').contains(instrument1.name); @@ -2247,7 +2247,7 @@ context('Technique Proposal tests', () => { cy.visit('/'); cy.finishedLoading(); - cy.contains('Xpress Proposals').click(); + cy.contains('Technique Proposals').click(); cy.contains(createdProposalId1); cy.should('not.contain', createdProposalId2); From d7cda6e897c2ab84c37d871202fc85f9b4b5200d Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Wed, 24 Jun 2026 12:22:39 +0100 Subject: [PATCH 08/12] try to fix e2e test --- apps/e2e/cypress/e2e/techniqueProposals.cy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts index 67b54c6e80..ff5e1e0002 100644 --- a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts +++ b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts @@ -2233,6 +2233,10 @@ context('Technique Proposal tests', () => { cy.contains('Technique Proposals').click(); + cy.get('[data-cy="call-filter"]').click(); + cy.get('[role="listbox"]').contains('All').click(); + cy.finishedLoading(); + cy.get('[data-cy="instrument-filter"]').click(); cy.get('[role="listbox"]').contains(instrument1.name); cy.get('[role="listbox"]').contains(instrument2.name).should('not.exist'); From 5108c366669fcbfa2aac8b3fa463a0eb5c2174cd Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Thu, 25 Jun 2026 17:51:27 +0100 Subject: [PATCH 09/12] technique proposals are not filtered by instrument by default --- .../postgres/ProposalDataSource.ts | 14 ++-- .../src/resolvers/queries/ProposalsQuery.ts | 3 - .../TechniqueProposalTable.tsx | 75 +++++++------------ 3 files changed, 31 insertions(+), 61 deletions(-) diff --git a/apps/backend/src/datasources/postgres/ProposalDataSource.ts b/apps/backend/src/datasources/postgres/ProposalDataSource.ts index c1b7d23696..5dc5585630 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -1117,18 +1117,14 @@ export default class PostgresProposalDataSource implements ProposalDataSource { 'ins.instrument_id' ) .modify((query) => { - const instrumentIdsToFilter = Array.from( - new Set([ - ...(filter?.instrumentIds || []), - ...(filter?.instrumentId ? [filter.instrumentId] : []), - ]) - ); + const instrumentId = filter?.instrumentFilter?.instrumentId; - if (instrumentIdsToFilter.length > 0) { + if (instrumentId && !isNaN(instrumentId)) { query.join('instrument_has_proposals as ihp', function () { - this.on('ihp.proposal_pk', '=', 'proposals.proposal_pk').andOnIn( + this.on('ihp.proposal_pk', '=', 'proposals.proposal_pk').andOnVal( 'ihp.instrument_id', - instrumentIdsToFilter + '=', + instrumentId ); }); } diff --git a/apps/backend/src/resolvers/queries/ProposalsQuery.ts b/apps/backend/src/resolvers/queries/ProposalsQuery.ts index fe2204cd9f..ac939d844d 100644 --- a/apps/backend/src/resolvers/queries/ProposalsQuery.ts +++ b/apps/backend/src/resolvers/queries/ProposalsQuery.ts @@ -84,9 +84,6 @@ export class ProposalsFilter { @Field(() => Int, { nullable: true }) public instrumentId?: number; - @Field(() => [Int], { nullable: true }) - public instrumentIds?: number[]; - @Field(() => InstrumentFilterInput, { nullable: true }) public instrumentFilter?: InstrumentFilterInput; diff --git a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx index 79c3fe9140..8cd1723667 100644 --- a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx +++ b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx @@ -218,7 +218,6 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { console.log({ proposalStatusId }); const [proposalFilter, setProposalFilter] = useState({ callId, - instrumentIds, instrumentFilter: { instrumentId: instrument ? +instrument : null, showAllProposals: !instrument, @@ -254,7 +253,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { return prev; }); - setProposalFilter((prev) => ({ ...prev, callId, instrumentIds })); + setProposalFilter((prev) => ({ ...prev, callId })); refreshTableData(); } }, [callId, setSearchParams, setProposalFilter, refreshTableData]); @@ -739,7 +738,6 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { callId, callIds, instrumentFilter, - instrumentIds, techniqueFilter, proposalStatusId, text, @@ -752,41 +750,31 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { totalCount: number; } = { proposals: undefined, totalCount: 0 }; api() - .getInstrumentsMinimal() - .then((data) => { - return data.instruments?.instruments.map((i) => i.id); - }) - .then((fetchedInstrumentIds) => { - return api().getTechniqueScientistProposals({ - filter: { - callId, - callIds, - instrumentFilter, - instrumentIds: - instrumentIds != null && instrumentIds.length > 0 - ? instrumentIds - : fetchedInstrumentIds, - techniqueFilter, - proposalStatusId, - text, - referenceNumbers, - dateFilter, - excludeProposalStatusIds: - currentRole === UserRole.INSTRUMENT_SCIENTIST - ? [StatusCode.EXPIRED] - : [], - }, - sortField: orderBy?.orderByField, - 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, - }); + .getTechniqueScientistProposals({ + filter: { + callId, + callIds, + instrumentFilter, + techniqueFilter, + proposalStatusId, + text, + referenceNumbers, + dateFilter, + excludeProposalStatusIds: + currentRole === UserRole.INSTRUMENT_SCIENTIST + ? [StatusCode.EXPIRED] + : [], + }, + sortField: orderBy?.orderByField, + 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, }) .then((data) => { result.totalCount = @@ -888,15 +876,6 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { } } - if (filter.instrumentFilter?.instrumentId != null) { - updatedFilter.instrumentIds = [ - filter.instrumentFilter.instrumentId as number, - ]; - } else { - updatedFilter.instrumentId = null; - updatedFilter.instrumentIds = - allInstruments?.map((instrument) => instrument.id) || []; - } setProposalFilter(updatedFilter); refreshTableData(); }; @@ -920,7 +899,6 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { callId, callIds, instrumentFilter, - instrumentIds, techniqueFilter, proposalStatusId, text, @@ -938,7 +916,6 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { .getTechniqueScientistProposalsBasic({ filter: { callId, - instrumentIds, instrumentFilter, callIds, techniqueFilter, From 5eac23e18f089ee701fbd0479259c41a57fc16a5 Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Fri, 26 Jun 2026 10:56:49 +0100 Subject: [PATCH 10/12] corrects technical proposal tag e2e tests --- apps/e2e/cypress/e2e/techniqueProposals.cy.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts index ff5e1e0002..ac3464afbd 100644 --- a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts +++ b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts @@ -2168,11 +2168,11 @@ context('Technique Proposal tests', () => { cy.get('[role="listbox"]').contains('All').click(); cy.finishedLoading(); - cy.contains(createdProposalId1); - cy.contains(createdProposalId2); - cy.contains(createdProposalId3); - cy.contains(createdProposalId4); - cy.contains(createdProposalId5); + cy.contains(proposal1.title); + cy.contains(proposal2.title); + cy.contains(proposal3.title); + cy.should('not.contain', proposal4.title); + cy.contains(proposal4.title); }); it('Derived User Officer role can only see appropriately tagged call filter options', () => { @@ -2253,11 +2253,11 @@ context('Technique Proposal tests', () => { cy.contains('Technique Proposals').click(); - cy.contains(createdProposalId1); - cy.should('not.contain', createdProposalId2); - cy.should('not.contain', createdProposalId3); - cy.should('not.contain', createdProposalId4); - cy.should('not.contain', createdProposalId5); + cy.contains(proposal1.title); + cy.should('not.contain', proposal2.title); + cy.should('not.contain', proposal3.title); + cy.should('not.contain', proposal4.title); + cy.should('not.contain', proposal5.title); }); }); }); From 53945ec0be3b873c87cfd2cce4ae45b3b8a0ca9c Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Fri, 26 Jun 2026 11:53:44 +0100 Subject: [PATCH 11/12] corrects e2e test case --- apps/e2e/cypress/e2e/techniqueProposals.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts index ac3464afbd..8674262180 100644 --- a/apps/e2e/cypress/e2e/techniqueProposals.cy.ts +++ b/apps/e2e/cypress/e2e/techniqueProposals.cy.ts @@ -2172,7 +2172,7 @@ context('Technique Proposal tests', () => { cy.contains(proposal2.title); cy.contains(proposal3.title); cy.should('not.contain', proposal4.title); - cy.contains(proposal4.title); + cy.contains(proposal5.title); }); it('Derived User Officer role can only see appropriately tagged call filter options', () => { From 3a39dfa2adb27781e375d40e6748b9c66b271dfe Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Wed, 1 Jul 2026 14:18:07 +0100 Subject: [PATCH 12/12] derived user officers can only assign technical proposals to instruments they are tagged to --- .../components/techniqueProposal/TechniqueProposalTable.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx index 0a04e10854..90804850f9 100644 --- a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx +++ b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx @@ -453,6 +453,12 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { instruments = [missingInst, ...instruments]; } + if (currentRole === UserRole.USER_OFFICER) { + instruments = instruments.filter((i) => + techniqueInstruments.map((ti) => ti.id).includes(i.id) + ); + } + // Always show the current instrument at the top of the dropdown instruments.forEach(function (instrument, i) { if (fieldValue && instrument.id === fieldValue) {