diff --git a/apps/backend/src/resolvers/types/Proposal.ts b/apps/backend/src/resolvers/types/Proposal.ts index 5420c5dc4d..4eaabc00f3 100644 --- a/apps/backend/src/resolvers/types/Proposal.ts +++ b/apps/backend/src/resolvers/types/Proposal.ts @@ -312,12 +312,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/e2e/cypress/e2e/proposals.cy.ts b/apps/e2e/cypress/e2e/proposals.cy.ts index 996a33691e..7f2a8908b9 100644 --- a/apps/e2e/cypress/e2e/proposals.cy.ts +++ b/apps/e2e/cypress/e2e/proposals.cy.ts @@ -192,6 +192,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/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..ab6f9d362e 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,25 @@ 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..4ab4ccdd08 --- /dev/null +++ b/apps/frontend/src/components/user/UserList.tsx @@ -0,0 +1,72 @@ +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; + 'data-cy'?: string; +} + +const StyledList = styled(List)(() => ({ + padding: 0, + '& .MuiListItem-root': { + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + }, +})); + +const UserList: React.FC = ({ + users, + initVisibleItems = 4, + 'data-cy': dataCy = 'user-list', +}) => { + 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..a57804e545 --- /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 { getFullUserNameWithEmail } 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'