From 7b146f6921b28215e0a9d33165adeac8f1678c00 Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Thu, 28 Sep 2023 13:46:29 +0300 Subject: [PATCH] HAI-1512 Implement sending invitation links again in user rights view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../accessRights/AccessRightsView.module.scss | 31 ++++- .../accessRights/AccessRightsView.test.tsx | 93 ++++++++++++++ .../hanke/accessRights/AccessRightsView.tsx | 116 +++++++++++++++++- src/domain/hanke/hankeUsers/hankeUser.ts | 1 + src/domain/hanke/hankeUsers/hankeUsersApi.ts | 5 + src/domain/mocks/handlers.ts | 5 + src/locales/fi.json | 13 +- 7 files changed, 253 insertions(+), 11 deletions(-) diff --git a/src/domain/hanke/accessRights/AccessRightsView.module.scss b/src/domain/hanke/accessRights/AccessRightsView.module.scss index ec258f4ad..b78804330 100644 --- a/src/domain/hanke/accessRights/AccessRightsView.module.scss +++ b/src/domain/hanke/accessRights/AccessRightsView.module.scss @@ -50,18 +50,43 @@ .table > div { overflow-x: initial; + display: none; - @include respond-below(m) { - display: none; + @include respond-above(l) { + display: block; } } .userCards { - @include respond-above(m) { + @include respond-above(l) { display: none; } } +.accessRightSelect { + min-width: auto; + + @include respond-above(m) { + min-width: 250px; + } + + @include respond-above(l) { + min-width: 330px; + } +} + +.invitationSendButtonContainer { + min-width: auto; + + @include respond-above(xl) { + min-width: 300px; + } +} + +.invitationSendButton { + vertical-align: middle; +} + .pagination { margin-top: var(--spacing-xs); margin-bottom: var(--spacing-s); diff --git a/src/domain/hanke/accessRights/AccessRightsView.test.tsx b/src/domain/hanke/accessRights/AccessRightsView.test.tsx index f9c44ddd7..38316ae5d 100644 --- a/src/domain/hanke/accessRights/AccessRightsView.test.tsx +++ b/src/domain/hanke/accessRights/AccessRightsView.test.tsx @@ -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(); + + 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(); + + 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 teppo@test.com.'), + ).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(); + + 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(); + + 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( + getSignedInUser({ kayttooikeustaso: 'KATSELUOIKEUS', kayttooikeudet: ['VIEW'] }), + ), + ); + }), + ); + + render(); + + await waitForLoadingToFinish(); + + expect( + screen.queryAllByRole('button', { + name: 'Lähetä kutsulinkki uudelleen', + }), + ).toHaveLength(0); +}); diff --git a/src/domain/hanke/accessRights/AccessRightsView.tsx b/src/domain/hanke/accessRights/AccessRightsView.tsx index 2e6358c73..a25a653bf 100644 --- a/src/domain/hanke/accessRights/AccessRightsView.tsx +++ b/src/domain/hanke/accessRights/AccessRightsView.tsx @@ -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[] = 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([]); 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 ( + + +

{t('hankeUsers:userIdentified')}

+
+ ); + } + + 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 ( +
+ +
+ ); + } + function updateUsers() { const users: Pick[] = 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 (
@@ -309,7 +374,10 @@ function AccessRightsView({ hankeUsers, hankeTunnus, hankeName, signedInUser }: {page.map((row) => { return ( - {getAccessRightSelect(row.original)} + {getAccessRightSelect(row.original)} + {signedInUser?.kayttooikeudet.includes('RESEND_INVITATION') + ? getInvitationResendButton(row.original) + : null} ); })} @@ -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()} > @@ -368,6 +436,42 @@ function AccessRightsView({ hankeUsers, hankeTunnus, hankeName, signedInUser }: )} + + {resendInvitationMutation.isSuccess && ( + resendInvitationMutation.reset()} + > + {t('hankeUsers:notifications:invitationSentSuccessText', { + email: hankeUsers.find((user) => user.id === resendInvitationMutation.data) + ?.sahkoposti, + })} + + )} + {resendInvitationMutation.isError && ( + resendInvitationMutation.reset()} + > + +

+ Kutsulinkin lähettämisessä tapahtui virhe. Yritä myöhemmin uudelleen tai ota + yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa + haitatontuki@hel.fi. +

+
+
+ )}
); diff --git a/src/domain/hanke/hankeUsers/hankeUser.ts b/src/domain/hanke/hankeUsers/hankeUser.ts index b64ec1faa..81df645e6 100644 --- a/src/domain/hanke/hankeUsers/hankeUser.ts +++ b/src/domain/hanke/hankeUsers/hankeUser.ts @@ -23,6 +23,7 @@ export type UserRights = Array< | 'MODIFY_DELETE_PERMISSIONS' | 'EDIT_APPLICATIONS' | 'MODIFY_APPLICATION_PERMISSIONS' + | 'RESEND_INVITATION' >; export type SignedInUser = { diff --git a/src/domain/hanke/hankeUsers/hankeUsersApi.ts b/src/domain/hanke/hankeUsers/hankeUsersApi.ts index af7e728c6..9fab334a9 100644 --- a/src/domain/hanke/hankeUsers/hankeUsersApi.ts +++ b/src/domain/hanke/hankeUsers/hankeUsersApi.ts @@ -27,3 +27,8 @@ export async function identifyUser(id: string) { const { data } = await api.post('kayttajat', { tunniste: id }); return data; } + +export async function resendInvitation(kayttajaId: string) { + await api.post(`kayttajat/${kayttajaId}/kutsu`); + return kayttajaId; +} diff --git a/src/domain/mocks/handlers.ts b/src/domain/mocks/handlers.ts index 9178c69a9..aa01afc04 100644 --- a/src/domain/mocks/handlers.ts +++ b/src/domain/mocks/handlers.ts @@ -200,6 +200,7 @@ export const handlers = [ 'MODIFY_DELETE_PERMISSIONS', 'EDIT_APPLICATIONS', 'MODIFY_APPLICATION_PERMISSIONS', + 'RESEND_INVITATION', ], }), ); @@ -215,4 +216,8 @@ export const handlers = [ }), ); }), + + rest.post(`${apiUrl}/kayttajat/:kayttajaId/kutsu`, async (req, res, ctx) => { + return res(ctx.delay(), ctx.status(204)); + }), ]; diff --git a/src/locales/fi.json b/src/locales/fi.json index 9c04852d1..702d6ce2d 100644 --- a/src/locales/fi.json +++ b/src/locales/fi.json @@ -861,8 +861,17 @@ "rightsUpdatedErrorText": "<0>Käyttöoikeuksien päivityksessä tapahtui virhe. Yritä myöhemmin uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa <1>haitatontuki@hel.fi.", "userIdentifiedLabel": "Tunnistautuminen onnistui", "userIdentifiedText": "Tunnistautuminen onnistui. Sinut on nyt lisätty hankkeelle ({{hankeNimi}} {{hankeTunnus}}).", - "userIdentifiedError": "Tunnistautuminen epäonnistui" - } + "userIdentifiedError": "Tunnistautuminen epäonnistui", + "invitationSentSuccessLabel": "Kutsulinkki lähetetty", + "invitationSentSuccessText": "Kutsulinkki lähetetty osoitteeseen {{email}}.", + "invitationSentErrorLabel": "Virhe linkin lähettämisessä", + "invitationSentErrorText": "<0>Kutsulinkin lähettämisessä tapahtui virhe. Yritä myöhemmin uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa <1>haitatontuki@hel.fi." + }, + "buttons": { + "resendInvitation": "Lähetä kutsulinkki uudelleen", + "invitationSent": "Kutsulinkki lähetetty" + }, + "userIdentified": "Käyttäjä tunnistautunut" }, "map": { "controls": {