Skip to content

Commit

Permalink
HAI-1512 Implement sending invitation links again in user rights view
Browse files Browse the repository at this point in the history
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
markohaarni committed Sep 28, 2023
1 parent 50d92f1 commit 7b146f6
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 11 deletions.
31 changes: 28 additions & 3 deletions src/domain/hanke/accessRights/AccessRightsView.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
93 changes: 93 additions & 0 deletions src/domain/hanke/accessRights/AccessRightsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
});
116 changes: 110 additions & 6 deletions src/domain/hanke/accessRights/AccessRightsView.tsx
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,
Expand All @@ -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,
Expand All @@ -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';

Expand Down Expand Up @@ -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();
Expand All @@ -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 {
Expand Down Expand Up @@ -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('');

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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}>
Expand Down Expand Up @@ -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>
);
})}
Expand Down Expand Up @@ -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">
Expand All @@ -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>
);
Expand Down
1 change: 1 addition & 0 deletions src/domain/hanke/hankeUsers/hankeUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type UserRights = Array<
| 'MODIFY_DELETE_PERMISSIONS'
| 'EDIT_APPLICATIONS'
| 'MODIFY_APPLICATION_PERMISSIONS'
| 'RESEND_INVITATION'
>;

export type SignedInUser = {
Expand Down
5 changes: 5 additions & 0 deletions src/domain/hanke/hankeUsers/hankeUsersApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ export async function identifyUser(id: string) {
const { data } = await api.post<IdentificationResponse>('kayttajat', { tunniste: id });
return data;
}

export async function resendInvitation(kayttajaId: string) {
await api.post(`kayttajat/${kayttajaId}/kutsu`);
return kayttajaId;
}
5 changes: 5 additions & 0 deletions src/domain/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export const handlers = [
'MODIFY_DELETE_PERMISSIONS',
'EDIT_APPLICATIONS',
'MODIFY_APPLICATION_PERMISSIONS',
'RESEND_INVITATION',
],
}),
);
Expand All @@ -215,4 +216,8 @@ export const handlers = [
}),
);
}),

rest.post(`${apiUrl}/kayttajat/:kayttajaId/kutsu`, async (req, res, ctx) => {
return res(ctx.delay(), ctx.status(204));
}),
];
Loading

0 comments on commit 7b146f6

Please sign in to comment.