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

HAI-1512 Implement sending invitation links again in user rights view #378

Merged
merged 3 commits into from
Oct 3, 2023
Merged
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
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: 345px;
}
}

.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 clicking 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
Loading