-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
HAI-1512 Implement sending invitation links again in user rights view
Added button for each user in the list to send invitation link to that user again if that user has not been identified in Haitaton before. When the button is pressed it is disabled and it's text is changed to Kutsulinkki lähetetty until the user visits the page again.
- Loading branch information
1 parent
50d92f1
commit 7b146f6
Showing
7 changed files
with
253 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import AccessRightsViewContainer from './AccessRightsViewContainer'; | |
import { server } from '../../mocks/test-server'; | ||
import usersData from '../../mocks/data/users-data.json'; | ||
import { SignedInUser } from '../hankeUsers/hankeUser'; | ||
import * as hankeUsersApi from '../../hanke/hankeUsers/hankeUsersApi'; | ||
|
||
jest.setTimeout(40000); | ||
|
||
|
@@ -302,3 +303,95 @@ test('Should not be able to remove all rights if user does not have all rights', | |
|
||
expect(screen.getByTestId('kayttooikeustaso-3').querySelector('button')).toBeDisabled(); | ||
}); | ||
|
||
test('Should show Käyttäjä tunnistautunut text for correct users', async () => { | ||
render(<AccessRightsViewContainer hankeTunnus="HAI22-2" />); | ||
|
||
await waitForLoadingToFinish(); | ||
|
||
expect(screen.getByTestId('tunnistautunut-0')).toHaveTextContent('Käyttäjä tunnistautunut'); | ||
expect(screen.getByTestId('tunnistautunut-1')).not.toHaveTextContent('Käyttäjä tunnistautunut'); | ||
expect(screen.getByTestId('tunnistautunut-2')).toHaveTextContent('Käyttäjä tunnistautunut'); | ||
expect(screen.getByTestId('tunnistautunut-6')).toHaveTextContent('Käyttäjä tunnistautunut'); | ||
expect(screen.getByTestId('tunnistautunut-7')).toHaveTextContent('Käyttäjä tunnistautunut'); | ||
expect(screen.getByTestId('tunnistautunut-8')).toHaveTextContent('Käyttäjä tunnistautunut'); | ||
}); | ||
|
||
test('Should send invitation to user when cliking the Lähetä kutsulinkki uudelleen button', async () => { | ||
const { user } = render(<AccessRightsViewContainer hankeTunnus="HAI22-2" />); | ||
|
||
await waitForLoadingToFinish(); | ||
|
||
const invitationButton = screen.getAllByRole('button', { | ||
name: 'Lähetä kutsulinkki uudelleen', | ||
})[0]; | ||
await user.click(invitationButton); | ||
|
||
await waitFor(() => { | ||
expect( | ||
screen.queryByText('Kutsulinkki lähetetty osoitteeseen [email protected].'), | ||
).toBeInTheDocument(); | ||
}); | ||
expect(invitationButton).toHaveTextContent('Kutsulinkki lähetetty'); | ||
expect(invitationButton).toBeDisabled(); | ||
}); | ||
|
||
test('Should not send multiple requests when cliking the Lähetä kutsulinkki uudelleen button many times', async () => { | ||
const sendInvitation = jest.spyOn(hankeUsersApi, 'resendInvitation'); | ||
const { user } = render(<AccessRightsViewContainer hankeTunnus="HAI22-2" />); | ||
|
||
await waitForLoadingToFinish(); | ||
|
||
const invitationButton = screen.getAllByRole('button', { | ||
name: 'Lähetä kutsulinkki uudelleen', | ||
})[0]; | ||
await user.click(invitationButton); | ||
await user.click(invitationButton); | ||
await user.click(invitationButton); | ||
|
||
expect(sendInvitation).toHaveBeenCalledTimes(1); | ||
|
||
sendInvitation.mockRestore(); | ||
}); | ||
|
||
test('Should show error notification if sending invitation fails', async () => { | ||
server.use( | ||
rest.post('/api/kayttajat/:kayttajaId/kutsu', async (req, res, ctx) => { | ||
return res(ctx.status(500), ctx.json({ errorMessage: 'Failed for testing purposes' })); | ||
}), | ||
); | ||
|
||
const { user } = render(<AccessRightsViewContainer hankeTunnus="HAI22-2" />); | ||
|
||
await waitForLoadingToFinish(); | ||
|
||
const invitationButton = screen.getAllByRole('button', { | ||
name: 'Lähetä kutsulinkki uudelleen', | ||
})[1]; | ||
await user.click(invitationButton); | ||
|
||
expect(screen.queryByText('Virhe linkin lähettämisessä')).toBeInTheDocument(); | ||
}); | ||
|
||
test('Should not show invitation buttons if user does not have permission to send invitation', async () => { | ||
server.use( | ||
rest.get('/api/hankkeet/:hankeTunnus/whoami', async (req, res, ctx) => { | ||
return res( | ||
ctx.status(200), | ||
ctx.json<SignedInUser>( | ||
getSignedInUser({ kayttooikeustaso: 'KATSELUOIKEUS', kayttooikeudet: ['VIEW'] }), | ||
), | ||
); | ||
}), | ||
); | ||
|
||
render(<AccessRightsViewContainer hankeTunnus="HAI22-2" />); | ||
|
||
await waitForLoadingToFinish(); | ||
|
||
expect( | ||
screen.queryAllByRole('button', { | ||
name: 'Lähetä kutsulinkki uudelleen', | ||
}), | ||
).toHaveLength(0); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import React, { useEffect, useMemo, useState } from 'react'; | ||
import React, { useEffect, useMemo, useRef, useState } from 'react'; | ||
import { | ||
Accordion, | ||
Button, | ||
|
@@ -10,9 +10,11 @@ import { | |
Select, | ||
Table, | ||
Link as HDSLink, | ||
IconEnvelope, | ||
IconCheckCircleFill, | ||
} from 'hds-react'; | ||
import { cloneDeep } from 'lodash'; | ||
import { Flex } from '@chakra-ui/react'; | ||
import { Box, Flex } from '@chakra-ui/react'; | ||
import { useMutation, useQueryClient } from 'react-query'; | ||
import { | ||
Column, | ||
|
@@ -31,7 +33,7 @@ import styles from './AccessRightsView.module.scss'; | |
import { Language } from '../../../common/types/language'; | ||
import { HankeUser, AccessRightLevel, SignedInUser } from '../hankeUsers/hankeUser'; | ||
import useHankeViewPath from '../hooks/useHankeViewPath'; | ||
import { updateHankeUsers } from '../hankeUsers/hankeUsersApi'; | ||
import { resendInvitation, updateHankeUsers } from '../hankeUsers/hankeUsersApi'; | ||
import Container from '../../../common/components/container/Container'; | ||
import UserCard from './UserCard'; | ||
|
||
|
@@ -59,6 +61,7 @@ type AccessRightLevelOption = { | |
const NAME_KEY = 'nimi'; | ||
const EMAIL_KEY = 'sahkoposti'; | ||
const ACCESS_RIGHT_LEVEL_KEY = 'kayttooikeustaso'; | ||
const USER_IDENTIFIED_KEY = 'tunnistautunut'; | ||
|
||
function AccessRightsView({ hankeUsers, hankeTunnus, hankeName, signedInUser }: Props) { | ||
const queryClient = useQueryClient(); | ||
|
@@ -69,7 +72,12 @@ function AccessRightsView({ hankeUsers, hankeTunnus, hankeName, signedInUser }: | |
const saveButtonDisabled = modifiedUsers.length === 0; | ||
|
||
const columns: Column<HankeUser>[] = useMemo(() => { | ||
return [{ accessor: NAME_KEY }, { accessor: EMAIL_KEY }, { accessor: ACCESS_RIGHT_LEVEL_KEY }]; | ||
return [ | ||
{ accessor: NAME_KEY }, | ||
{ accessor: EMAIL_KEY }, | ||
{ accessor: ACCESS_RIGHT_LEVEL_KEY }, | ||
{ accessor: USER_IDENTIFIED_KEY }, | ||
]; | ||
}, []); | ||
|
||
const { | ||
|
@@ -102,6 +110,9 @@ function AccessRightsView({ hankeUsers, hankeTunnus, hankeName, signedInUser }: | |
}, [hankeUsers]); | ||
|
||
const updateUsersMutation = useMutation(updateHankeUsers); | ||
const resendInvitationMutation = useMutation(resendInvitation); | ||
// List of user ids for tracking which users have been sent the invitation link | ||
const linksSentTo = useRef<string[]>([]); | ||
|
||
const [usersSearchValue, setUsersSearchValue] = useState(''); | ||
|
||
|
@@ -177,10 +188,56 @@ function AccessRightsView({ hankeUsers, hankeTunnus, hankeName, signedInUser }: | |
option.value === 'KAIKKI_OIKEUDET' && signedInUser?.kayttooikeustaso !== 'KAIKKI_OIKEUDET' | ||
} | ||
disabled={isDisabled} | ||
className={styles.accessRightSelect} | ||
/> | ||
); | ||
} | ||
|
||
function getInvitationResendButton(args: HankeUser) { | ||
if (args.tunnistautunut) { | ||
return ( | ||
<Flex alignItems="baseline"> | ||
<IconCheckCircleFill | ||
color="var(--color-success)" | ||
style={{ marginRight: 'var(--spacing-3-xs)', alignSelf: 'center' }} | ||
aria-hidden | ||
/> | ||
<p>{t('hankeUsers:userIdentified')}</p> | ||
</Flex> | ||
); | ||
} | ||
|
||
const linkSent = linksSentTo.current.includes(args.id); | ||
const buttonText = linkSent | ||
? t('hankeUsers:buttons:invitationSent') | ||
: t('hankeUsers:buttons:resendInvitation'); | ||
const isButtonLoading = | ||
resendInvitationMutation.isLoading && resendInvitationMutation.variables === args.id; | ||
|
||
function sendInvitation() { | ||
resendInvitationMutation.mutate(args.id, { | ||
onSuccess(data) { | ||
linksSentTo.current.push(data); | ||
}, | ||
}); | ||
} | ||
|
||
return ( | ||
<div className={styles.invitationSendButtonContainer}> | ||
<Button | ||
variant="secondary" | ||
className={styles.invitationSendButton} | ||
iconLeft={<IconEnvelope aria-hidden />} | ||
onClick={sendInvitation} | ||
disabled={linkSent} | ||
isLoading={isButtonLoading} | ||
> | ||
{buttonText} | ||
</Button> | ||
</div> | ||
); | ||
} | ||
|
||
function updateUsers() { | ||
const users: Pick<HankeUser, 'id' | 'kayttooikeustaso'>[] = modifiedUsers.map((user) => { | ||
return { | ||
|
@@ -218,6 +275,14 @@ function AccessRightsView({ hankeUsers, hankeTunnus, hankeName, signedInUser }: | |
}, | ||
]; | ||
|
||
if (signedInUser?.kayttooikeudet.includes('RESEND_INVITATION')) { | ||
tableCols.push({ | ||
headerName: '', | ||
key: USER_IDENTIFIED_KEY, | ||
transform: getInvitationResendButton, | ||
}); | ||
} | ||
|
||
return ( | ||
<article className={styles.container}> | ||
<header className={styles.header}> | ||
|
@@ -309,7 +374,10 @@ function AccessRightsView({ hankeUsers, hankeTunnus, hankeName, signedInUser }: | |
{page.map((row) => { | ||
return ( | ||
<UserCard key={row.original.id} user={row.original}> | ||
{getAccessRightSelect(row.original)} | ||
<Box marginBottom="var(--spacing-s)">{getAccessRightSelect(row.original)}</Box> | ||
{signedInUser?.kayttooikeudet.includes('RESEND_INVITATION') | ||
? getInvitationResendButton(row.original) | ||
: null} | ||
</UserCard> | ||
); | ||
})} | ||
|
@@ -356,7 +424,7 @@ function AccessRightsView({ hankeUsers, hankeTunnus, hankeName, signedInUser }: | |
dismissible | ||
type="error" | ||
label={t('hankeUsers:notifications:rightsUpdatedErrorLabel')} | ||
closeButtonLabelText={t('hankeUsers:notifications:rightsUpdatedErrorLabel')} | ||
closeButtonLabelText={t('common:components:notification:closeButtonLabelText')} | ||
onClose={() => updateUsersMutation.reset()} | ||
> | ||
<Trans i18nKey="hankeUsers:notifications:rightsUpdatedErrorText"> | ||
|
@@ -368,6 +436,42 @@ function AccessRightsView({ hankeUsers, hankeTunnus, hankeName, signedInUser }: | |
</Trans> | ||
</Notification> | ||
)} | ||
|
||
{resendInvitationMutation.isSuccess && ( | ||
<Notification | ||
position="top-right" | ||
dismissible | ||
autoClose | ||
autoCloseDuration={4000} | ||
type="success" | ||
label={t('hankeUsers:notifications:invitationSentSuccessLabel')} | ||
closeButtonLabelText={t('common:components:notification:closeButtonLabelText')} | ||
onClose={() => resendInvitationMutation.reset()} | ||
> | ||
{t('hankeUsers:notifications:invitationSentSuccessText', { | ||
email: hankeUsers.find((user) => user.id === resendInvitationMutation.data) | ||
?.sahkoposti, | ||
})} | ||
</Notification> | ||
)} | ||
{resendInvitationMutation.isError && ( | ||
<Notification | ||
position="top-right" | ||
dismissible | ||
type="error" | ||
label={t('hankeUsers:notifications:invitationSentErrorLabel')} | ||
closeButtonLabelText={t('common:components:notification:closeButtonLabelText')} | ||
onClose={() => resendInvitationMutation.reset()} | ||
> | ||
<Trans i18nKey="hankeUsers:notifications:invitationSentErrorText"> | ||
<p> | ||
Kutsulinkin lähettämisessä tapahtui virhe. Yritä myöhemmin uudelleen tai ota | ||
yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa | ||
<HDSLink href="mailto:[email protected]">[email protected]</HDSLink>. | ||
</p> | ||
</Trans> | ||
</Notification> | ||
)} | ||
</Container> | ||
</article> | ||
); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.