Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EASI-4551 Add a system team member #2819

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/hooks/useIsWorkspaceParam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useLocation } from 'react-router-dom';

export default function useIsWorkspaceParam(): boolean {
const loc = useLocation();
const par = new URLSearchParams(loc.search);
return par.has('workspace');
}
41 changes: 35 additions & 6 deletions src/i18n/en-US/systemProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,14 @@ const systemProfile = {
add: {
title: 'Add a team member',
description:
'Look up your team member and then add their role(s) on this team.',
buttonLabel: 'Add this team member',
'Search for a team member to add to your system team. Team members are all able to view and edit your System Profile, including adding or removing team members. Team members can also create and work on IT Governance and technical assistance requests.',
buttonLabel: 'Add team member',
returnButtonLabel:
'Don’t add a team member and return to previous page'
'Don’t add a team member (return to previous page)',
alertInfo:
'This new team member will be able to edit information about your system within EASi. Please make sure this individual should be able to do this before you proceed.',
memberAlreadySelectedInfo:
'This individual has already been added as a team member. Their existing roles are populated below. Adding or removing roles will change the roles assigned to this individual for this system.'
},
edit: {
title: 'Edit team member roles',
Expand All @@ -235,11 +239,11 @@ const systemProfile = {
},
name: 'Team member name',
nameDescription:
'Search by name. Looking up your team member will provide their name and email address.',
'Search by name. This field searches CMS’ EUA database. Looking up your team member will provide their name and email address.',
nameError: 'Team member name is a required field',
roles: 'Team member role(s)',
rolesDescription:
'Add or remove roles by clicking in the box below. You must select at least one role for this team member.',
'Add or remove roles by clicking in the box below. Select all that apply. You must select at least one role for this team member.',
rolesError: 'You must select at least one role for this team member',
selectedRoles: 'Selected roles',
successUpdateRoles: 'Roles for {{commonName}} have been updated.',
Expand All @@ -251,7 +255,32 @@ const systemProfile = {
successRemoveContact:
'{{commonName}} has been removed as a team member.',
errorRemoveContact:
'There was a problem removing a team member. Please try again. If the error persists, please try again at a later date.'
'There was a problem removing a team member. Please try again. If the error persists, please try again at a later date.',
availableRoles: {
link: 'What roles are available?',
primaryLabel: 'Primary roles',
pocLabel: 'Points of contact',
pocText:
'These are additional roles to signify primary points of contact for specific topics as appropriate. They identify who is best to contact if someone has questions on those topics.',
primaryList: [
'<strong>Business Owner:</strong> A person on the business owner team for the system or the person officially responsible for the business decisions of the system and the budgeting for the system. Every system should have at least one Business Owner, and this role must be a federal employee.',
'<strong>System Maintainer:</strong> A lead of the system maintainer team for the system or the person responsible for overseeing the technical operations of the system. Every system should have at least one System Maintainer, and this role must be a federal employee.',
"<strong>Contracting Officer's Representative (COR):</strong> A person responsible for all contracting tasks and activities related to the one or more system contracts. Every system should have at least one of the following: COR, GTL, or Project Lead, and that role must be filled by a federal employee.",
'<strong>Government Task Lead (GTL):</strong> A person responsible for leading the work on one or more task orders on a contract for the system. Every system should have at least one of the following: COR, GTL, or Project Lead, and that role must be filled by a federal employee.',
'<strong>Project Lead:</strong> A Project Manager or person responsible for leading the day-to-day work on a project or multiple projects for the system. Every system should have at least one of the following: COR, GTL, or Project Lead, and that role must be filled by a federal employee.',
'<strong>Information System Security Officer (ISSO):</strong> A person who is the security specialist for the system. This person is responsible for leading any security compliance procedures required for the system. An ISSO may often be an OIT staff member and may work on multiple systems.',
'<strong>Subject Matter Expert (SME):</strong> Additional staff who provide policy, process, or business expertise to a system.',
'<strong>Budget Analyst:</strong> The person who prepares and submits the annual budget request for the system.',
'<strong>Support Staff:</strong> A person who supports this system as a team member or in another capacity. Select this option if none of the other role options are applicable.'
],
pocList: [
'<strong>Business Question Contact:</strong> A contact who can answer business questions for the system. Every system should have a team member with this role.',
'<strong>Technical System Issues Contact:</strong> The person on the team to contact if technical issues are found with the system. Every system should have a team member with this role.',
'<strong>Data Center Contact:</strong> The person on the team to contact if there is a question regarding the system’s data center or hosting environment.',
'<strong>API Contact:</strong> A person knowledgeable about API related information for the system.',
'<strong>AI Contact:</strong> A person knowledgeable about Artificial Intelligence (AI) and/or Machine Learning (ML) related information for the system.'
]
}
}
},
editPage: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe('Edit team page', () => {
cedarSystemId="b7d0695d-4c24-4942-a815-77655f43783c"
updateRoles={mockUpdateRoles}
loading={false}
team={[]}
/>
</Route>
</MessageProvider>
Expand Down Expand Up @@ -122,6 +123,7 @@ describe('Edit team page', () => {
cedarSystemId="b7d0695d-4c24-4942-a815-77655f43783c"
updateRoles={mockUpdateRoles}
loading={false}
team={[]}
/>
</Route>
</MessageProvider>
Expand Down
134 changes: 123 additions & 11 deletions src/views/SystemProfile/components/Team/Edit/TeamMemberForm.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { useHistory, useLocation } from 'react-router-dom';
import { FetchResult, MutationFunctionOptions, useQuery } from '@apollo/client';
import { yupResolver } from '@hookform/resolvers/yup';
import {
Alert,
Button,
CardGroup,
Form,
Expand All @@ -14,18 +15,26 @@ import {
Label
} from '@trussworks/react-uswds';
import classNames from 'classnames';
import { useFlags } from 'launchdarkly-react-client-sdk';
import * as yup from 'yup';

import CedarContactSelect from 'components/CedarContactSelect';
import PageLoading from 'components/PageLoading';
import CollapsableLink from 'components/shared/CollapsableLink';
import FieldErrorMsg from 'components/shared/FieldErrorMsg';
import HelpText from 'components/shared/HelpText';
import IconLink from 'components/shared/IconLink';
import MultiSelect from 'components/shared/MultiSelect';
import RequiredAsterisk from 'components/shared/RequiredAsterisk';
import Spinner from 'components/Spinner';
import teamRolesIndex from 'constants/teamRolesIndex';
import useIsWorkspaceParam from 'hooks/useIsWorkspaceParam';
import useMessage from 'hooks/useMessage';
import { GetCedarRoleTypesQuery } from 'queries/CedarRoleQueries';
import { GetCedarRoleTypes } from 'queries/types/GetCedarRoleTypes';
import {
GetCedarRoleTypes,
GetCedarRoleTypes_roleTypes as CedarRoleTypes
} from 'queries/types/GetCedarRoleTypes';
import {
SetRolesForUserOnSystem,
SetRolesForUserOnSystemVariables
Expand All @@ -50,6 +59,7 @@ type TeamMemberFormProps = {
>
) => Promise<FetchResult<SetRolesForUserOnSystem>>;
loading: boolean;
team: UsernameWithRoles[];
};

const teamMemberSchema: yup.SchemaOf<TeamMemberFields> = yup.object({
Expand All @@ -63,13 +73,18 @@ const teamMemberSchema: yup.SchemaOf<TeamMemberFields> = yup.object({
const TeamMemberForm = ({
cedarSystemId,
updateRoles,
loading
loading,
team
}: TeamMemberFormProps) => {
const { t } = useTranslation('systemProfile');

const flags = useFlags();
const isWorkspace = useIsWorkspaceParam();

const { showMessageOnNextPage, showMessage } = useMessage();
const history = useHistory();
const { state } = useLocation<{ user?: UsernameWithRoles }>();
/** A defined user also indicates form edit mode. */
const user = state?.user;

/* User commonName prop used for setting success/error messages */
Expand All @@ -83,6 +98,27 @@ const TeamMemberForm = ({
GetCedarRoleTypesQuery
);

const availableRolesText = t<Record<string, string[]>>(
'singleSystem.editTeam.form.availableRoles',
{
returnObjects: true
}
);

const rolesOrdered: CedarRoleTypes[] = useMemo(() => {
const roles = data?.roleTypes;

if (roles === undefined) return [];

/** Hide any undefined roles. */
const knownRoles = Object.keys(teamRolesIndex());

return roles
.concat()
.filter(r => knownRoles.includes(r.name))
.sort((a, b) => teamRolesIndex()[a.name] - teamRolesIndex()[b.name]);
}, [data?.roleTypes]);

const {
control,
setValue,
Expand Down Expand Up @@ -139,6 +175,20 @@ const TeamMemberForm = ({
}
});

const euaUserId = watch('euaUserId');

const memberAlreadySelected =
euaUserId !== undefined && team.find(u => u.assigneeUsername === euaUserId);

useEffect(() => {
setValue(
'desiredRoleTypeIDs',
memberAlreadySelected
? memberAlreadySelected.roles.map(r => r.roleTypeID)
: []
);
}, [setValue, memberAlreadySelected]);

if (roleTypesLoading) {
return <PageLoading />;
}
Expand All @@ -149,20 +199,25 @@ const TeamMemberForm = ({
<p className="margin-bottom-6">{t(`${keyPrefix}.description`)}</p>

<Form onSubmit={submitForm} className="maxw-none">
{user ? (
{user && flags.systemWorkspaceTeam && !isWorkspace ? (
// If editing, show contact card without roles
// when in the original system profile context (not workspace)
// Rendered in non-workspace edit only
<CardGroup>
<TeamContactCard user={user} displayRoles={false} />
</CardGroup>
) : (
// If adding new contact, show CEDAR contact select field
// Or if in workspace context show user info in disabled dropdown
// Rendered in workspace context for both edit + add modes
<Controller
name="euaUserId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormGroup error={!!error}>
<Label htmlFor={field.name} className="margin-bottom-05">
{t('singleSystem.editTeam.form.name')}
<RequiredAsterisk />
</Label>
<HelpText>
{t('singleSystem.editTeam.form.nameDescription')}
Expand All @@ -175,7 +230,17 @@ const TeamMemberForm = ({
<CedarContactSelect
{...{ ...field, ref: null }}
id={field.name}
value={user}
// A defined `user` in edit mode will display as a disabled dropdown
value={
user
? {
euaUserId: user.assigneeUsername,
commonName,
email: user.roles[0].assigneeEmail || undefined
}
: undefined
}
disabled={!!user}
onChange={contact => {
if (contact) {
setValue('euaUserId', contact?.euaUserId);
Expand All @@ -185,11 +250,17 @@ const TeamMemberForm = ({
className="maxw-none"
// onChange={cedarContact => console.log(cedarContact)}
/>
{memberAlreadySelected && (
<Alert slim type="info">
{t(
'singleSystem.editTeam.form.add.memberAlreadySelectedInfo'
)}
</Alert>
)}
</FormGroup>
)}
/>
)}

{/* Role multiselect */}
<Controller
name="desiredRoleTypeIDs"
Expand All @@ -201,6 +272,7 @@ const TeamMemberForm = ({
>
<Label htmlFor={field.name} className="margin-bottom-05">
{t('singleSystem.editTeam.form.roles')}
<RequiredAsterisk />
</Label>
<HelpText>
{t('singleSystem.editTeam.form.rolesDescription')}
Expand All @@ -212,8 +284,9 @@ const TeamMemberForm = ({
)}
<MultiSelect
{...{ ...field, ref: null }}
key={field.value.join()} // Doing this to rerender on value change
name={field.name}
options={(data?.roleTypes || []).map(role => ({
options={rolesOrdered.map(role => ({
value: role.id,
label: role.name
}))}
Expand All @@ -222,7 +295,42 @@ const TeamMemberForm = ({
</FormGroup>
)}
/>

<CollapsableLink
id="availableRoles"
label={t('singleSystem.editTeam.form.availableRoles.link')}
>
<p className="padding-top-0">
<strong>
{t('singleSystem.editTeam.form.availableRoles.primaryLabel')}
</strong>
</p>
<ul className="easi-list padding-left-2">
{availableRolesText.primaryList.map(li => (
<li key={li}>
<Trans>{li}</Trans>
</li>
))}
</ul>
<p>
<strong>
{t('singleSystem.editTeam.form.availableRoles.pocLabel')}
</strong>
<br />
<span className="text-base-dark">
{t('singleSystem.editTeam.form.availableRoles.pocText')}
</span>
</p>
<ul className="easi-list padding-left-2">
{availableRolesText.pocList.map(li => (
<li key={li}>
<Trans>{li}</Trans>
</li>
))}
</ul>
</CollapsableLink>
<Alert slim type="info">
{t('singleSystem.editTeam.form.add.alertInfo')}
</Alert>
<div className="display-flex flex-align-center margin-top-6">
<Button
type="submit"
Expand All @@ -231,7 +339,7 @@ const TeamMemberForm = ({
loading ||
!isDirty ||
watch('desiredRoleTypeIDs').length === 0 ||
!watch('euaUserId')
!euaUserId
}
className="margin-0"
>
Expand All @@ -243,7 +351,11 @@ const TeamMemberForm = ({

<IconLink
icon={<IconArrowBack />}
to={`/systems/${cedarSystemId}/team/edit`}
to={
isWorkspace
? `/systems/${cedarSystemId}/workspace`
: `/systems/${cedarSystemId}/team/edit`
}
className="margin-top-3"
>
{t(`${keyPrefix}.returnButtonLabel`)}
Expand Down
Loading