From e14fe165dce9a64649b5daab6e1e636dd216dc1d Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Wed, 17 Dec 2025 11:10:51 +0100 Subject: [PATCH 1/3] feat: add data access users functionality to proposal management --- apps/backend/src/resolvers/types/Proposal.ts | 13 +++- .../components/proposal/ProposalCreate.tsx | 1 + .../review/ProposalQuestionaryReview.tsx | 26 +++++-- .../components/review/ReviewQuestionary.tsx | 1 + .../review/TechnicalReviewQuestionary.tsx | 1 + .../frontend/src/components/user/UserList.tsx | 67 +++++++++++++++++++ .../src/components/user/UserListItem.tsx | 22 ++++++ .../src/graphql/proposal/getProposal.graphql | 3 + .../proposal/ProposalWithQuestionary.ts | 1 + 9 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 apps/frontend/src/components/user/UserList.tsx create mode 100644 apps/frontend/src/components/user/UserListItem.tsx diff --git a/apps/backend/src/resolvers/types/Proposal.ts b/apps/backend/src/resolvers/types/Proposal.ts index f14402443e..c5adb86069 100644 --- a/apps/backend/src/resolvers/types/Proposal.ts +++ b/apps/backend/src/resolvers/types/Proposal.ts @@ -296,12 +296,23 @@ export class ProposalResolver { } @FieldResolver(() => ProposalAttachments, { nullable: true }) - attachments(@Root() proposal: Proposal, @Ctx() ctx: ResolverContext) { + async attachments(@Root() proposal: Proposal, @Ctx() ctx: ResolverContext) { return ctx.queries.questionary.getProposalAttachments( ctx.user, proposal.primaryKey ); } + + @FieldResolver(() => [BasicUserDetails], { nullable: true }) + async dataAccessUsers( + @Root() proposal: Proposal, + @Ctx() context: ResolverContext + ): Promise { + return await context.queries.dataAccessUsers.findByProposalPk( + context.user, + proposal.primaryKey + ); + } } export async function resolveProposalReference( diff --git a/apps/frontend/src/components/proposal/ProposalCreate.tsx b/apps/frontend/src/components/proposal/ProposalCreate.tsx index b1a6bd1d14..fddc16f21a 100644 --- a/apps/frontend/src/components/proposal/ProposalCreate.tsx +++ b/apps/frontend/src/components/proposal/ProposalCreate.tsx @@ -53,6 +53,7 @@ export function createProposalStub( samples: [], genericTemplates: [], call: call, + dataAccessUsers: [], }; } diff --git a/apps/frontend/src/components/review/ProposalQuestionaryReview.tsx b/apps/frontend/src/components/review/ProposalQuestionaryReview.tsx index f593a2132a..e388f4bb54 100644 --- a/apps/frontend/src/components/review/ProposalQuestionaryReview.tsx +++ b/apps/frontend/src/components/review/ProposalQuestionaryReview.tsx @@ -1,12 +1,14 @@ import { TableProps } from '@mui/material'; -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useContext } from 'react'; import UOLoader from 'components/common/UOLoader'; import ProposalQuestionaryDetails from 'components/proposal/ProposalQuestionaryDetails'; import { TableRowData } from 'components/questionary/QuestionaryDetails'; -import { BasicUserDetails } from 'generated/sdk'; +import UserList from 'components/user/UserList'; +import UserListItem from 'components/user/UserListItem'; +import { FeatureContext } from 'context/FeatureContextProvider'; +import { FeatureId } from 'generated/sdk'; import { ProposalWithQuestionary } from 'models/questionary/proposal/ProposalWithQuestionary'; -import { getFullUserNameWithBasicDetails } from 'utils/user'; export default function ProposalQuestionaryReview( props: { @@ -14,6 +16,10 @@ export default function ProposalQuestionaryReview( } & TableProps> ) { const { data, ...restProps } = props; + const featureContext = useContext(FeatureContext); + const isDataAccessUsersEnabled = featureContext.featuresMap.get( + FeatureId.DATA_ACCESS_USERS + )?.isEnabled; if (!data.questionaryId) { return ; @@ -34,14 +40,20 @@ export default function ProposalQuestionaryReview( { label: 'Abstract', value: data.abstract }, { label: 'Principal Investigator', - value: getFullUserNameWithBasicDetails(data.proposer), + value: , }, { label: 'Co-Proposers', - value: users - .map((user: BasicUserDetails) => getFullUserNameWithBasicDetails(user)) - .join(', '), + value: , }, + ...(isDataAccessUsersEnabled && data.dataAccessUsers + ? [ + { + label: 'Data Access Users', + value: , + }, + ] + : []), ...(data.coProposerInvites?.length > 0 ? [ { diff --git a/apps/frontend/src/components/review/ReviewQuestionary.tsx b/apps/frontend/src/components/review/ReviewQuestionary.tsx index a20940859a..d22a1c7691 100644 --- a/apps/frontend/src/components/review/ReviewQuestionary.tsx +++ b/apps/frontend/src/components/review/ReviewQuestionary.tsx @@ -101,6 +101,7 @@ export function createFapReviewStub( experiments: null, experimentSequence: null, fileId: null, + dataAccessUsers: [], }, }; } diff --git a/apps/frontend/src/components/review/TechnicalReviewQuestionary.tsx b/apps/frontend/src/components/review/TechnicalReviewQuestionary.tsx index 112cfe3b80..e0f729fb19 100644 --- a/apps/frontend/src/components/review/TechnicalReviewQuestionary.tsx +++ b/apps/frontend/src/components/review/TechnicalReviewQuestionary.tsx @@ -114,6 +114,7 @@ export function createTechnicalReviewStub( experiments: null, experimentSequence: null, fileId: null, + dataAccessUsers: [], }, }; } diff --git a/apps/frontend/src/components/user/UserList.tsx b/apps/frontend/src/components/user/UserList.tsx new file mode 100644 index 0000000000..34feb736ac --- /dev/null +++ b/apps/frontend/src/components/user/UserList.tsx @@ -0,0 +1,67 @@ +import { Button, Collapse, List, ListItem, styled } from '@mui/material'; +import React, { useState } from 'react'; + +import { BasicUserDetails } from 'generated/sdk'; + +import UserListItem from './UserListItem'; + +interface UserListProps { + users: BasicUserDetails[]; + initVisibleItems?: number; +} + +const StyledList = styled(List)(() => ({ + padding: 0, + '& .MuiListItem-root': { + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + }, +})); + +const UserList: React.FC = ({ users, initVisibleItems = 4 }) => { + const [showAll, setShowAll] = useState(false); + + if (!users || users.length === 0) { + return null; + } + + const initialUsers = users.slice(0, initVisibleItems); + const hiddenUsers = users.slice(initVisibleItems); + const hasMore = users.length > initVisibleItems; + + return ( +
+ + {initialUsers.map((user) => ( + + + + ))} + + + + {hiddenUsers.map((user) => ( + + + + ))} + + + {hasMore && ( + + )} +
+ ); +}; + +export default UserList; diff --git a/apps/frontend/src/components/user/UserListItem.tsx b/apps/frontend/src/components/user/UserListItem.tsx new file mode 100644 index 0000000000..46f6271283 --- /dev/null +++ b/apps/frontend/src/components/user/UserListItem.tsx @@ -0,0 +1,22 @@ +import { ListItemText } from '@mui/material'; +import React from 'react'; + +import { BasicUserDetails } from 'generated/sdk'; +import { getFullUserName } from 'utils/user'; + +interface UserListItemProps { + user?: BasicUserDetails | null; +} + +const UserListItem: React.FC = ({ user }) => { + return ( + + ); +}; + +export default UserListItem; diff --git a/apps/frontend/src/graphql/proposal/getProposal.graphql b/apps/frontend/src/graphql/proposal/getProposal.graphql index 2d44f50a4e..d1aa545a5f 100644 --- a/apps/frontend/src/graphql/proposal/getProposal.graphql +++ b/apps/frontend/src/graphql/proposal/getProposal.graphql @@ -7,6 +7,9 @@ query getProposal($primaryKey: Int!) { users { ...basicUserDetails } + dataAccessUsers { + ...basicUserDetails + } questionary { ...questionary isCompleted diff --git a/apps/frontend/src/models/questionary/proposal/ProposalWithQuestionary.ts b/apps/frontend/src/models/questionary/proposal/ProposalWithQuestionary.ts index 43e54a0cff..16985411cb 100644 --- a/apps/frontend/src/models/questionary/proposal/ProposalWithQuestionary.ts +++ b/apps/frontend/src/models/questionary/proposal/ProposalWithQuestionary.ts @@ -15,6 +15,7 @@ export type ProposalWithQuestionary = Pick< | 'questionary' | 'status' | 'users' + | 'dataAccessUsers' | 'coProposerInvites' | 'title' | 'proposalId' From 0355f6ce155ccf17397c8755723776c694fecc02 Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Wed, 17 Dec 2025 11:40:15 +0100 Subject: [PATCH 2/3] feat: add data access users list to proposal review and update UserList component for data-cy attribute --- apps/e2e/cypress/e2e/proposals.cy.ts | 3 +++ .../src/components/review/ProposalQuestionaryReview.tsx | 7 ++++++- apps/frontend/src/components/user/UserList.tsx | 9 +++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/e2e/cypress/e2e/proposals.cy.ts b/apps/e2e/cypress/e2e/proposals.cy.ts index 9de7f5d431..76881623fc 100644 --- a/apps/e2e/cypress/e2e/proposals.cy.ts +++ b/apps/e2e/cypress/e2e/proposals.cy.ts @@ -185,6 +185,9 @@ context('Proposal tests', () => { createdProposalId ); cy.get('[data-cy="questionary-details-view"]').contains(newProposalTitle); + cy.get('[data-cy="data-access-users-list"]').contains( + initialDBData.users.user3.lastName + ); }); it('Copy to clipboard should work for Proposal ID', () => { diff --git a/apps/frontend/src/components/review/ProposalQuestionaryReview.tsx b/apps/frontend/src/components/review/ProposalQuestionaryReview.tsx index e388f4bb54..ab6f9d362e 100644 --- a/apps/frontend/src/components/review/ProposalQuestionaryReview.tsx +++ b/apps/frontend/src/components/review/ProposalQuestionaryReview.tsx @@ -50,7 +50,12 @@ export default function ProposalQuestionaryReview( ? [ { label: 'Data Access Users', - value: , + value: ( + + ), }, ] : []), diff --git a/apps/frontend/src/components/user/UserList.tsx b/apps/frontend/src/components/user/UserList.tsx index 34feb736ac..4ab4ccdd08 100644 --- a/apps/frontend/src/components/user/UserList.tsx +++ b/apps/frontend/src/components/user/UserList.tsx @@ -8,6 +8,7 @@ import UserListItem from './UserListItem'; interface UserListProps { users: BasicUserDetails[]; initVisibleItems?: number; + 'data-cy'?: string; } const StyledList = styled(List)(() => ({ @@ -20,7 +21,11 @@ const StyledList = styled(List)(() => ({ }, })); -const UserList: React.FC = ({ users, initVisibleItems = 4 }) => { +const UserList: React.FC = ({ + users, + initVisibleItems = 4, + 'data-cy': dataCy = 'user-list', +}) => { const [showAll, setShowAll] = useState(false); if (!users || users.length === 0) { @@ -32,7 +37,7 @@ const UserList: React.FC = ({ users, initVisibleItems = 4 }) => { const hasMore = users.length > initVisibleItems; return ( -
+
{initialUsers.map((user) => ( From 24302ea26723ed1dbbb21ff0f421011eca28d3f1 Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Wed, 17 Dec 2025 12:48:48 +0100 Subject: [PATCH 3/3] fix: update UserListItem to use getFullUserNameWithEmail for primary display --- apps/frontend/src/components/user/UserListItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/user/UserListItem.tsx b/apps/frontend/src/components/user/UserListItem.tsx index 46f6271283..a57804e545 100644 --- a/apps/frontend/src/components/user/UserListItem.tsx +++ b/apps/frontend/src/components/user/UserListItem.tsx @@ -2,7 +2,7 @@ import { ListItemText } from '@mui/material'; import React from 'react'; import { BasicUserDetails } from 'generated/sdk'; -import { getFullUserName } from 'utils/user'; +import { getFullUserNameWithEmail } from 'utils/user'; interface UserListItemProps { user?: BasicUserDetails | null; @@ -11,7 +11,7 @@ interface UserListItemProps { const UserListItem: React.FC = ({ user }) => { return (