From 3496cc84f94284c4eb491c38bae99987c409113c Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Fri, 6 Oct 2023 10:24:33 +0300 Subject: [PATCH] HAI-1945 Don't show edit hanke link buttons in hanke list if user does not have edit rights (#379) Edit hanke link button (pen icon) is hidden in hanke list if user does not have edit rights for that hanke. --- .../AccessRightsViewContainer.tsx | 4 +- .../hanke/hankeUsers/UserRightsCheck.test.tsx | 43 +++++++++++++++++++ .../hanke/hankeUsers/UserRightsCheck.tsx | 29 +++++++++++++ src/domain/hanke/hankeUsers/hankeUser.ts | 24 ++++++----- src/domain/hanke/hankeUsers/hankeUsersApi.ts | 4 +- .../hanke/hankeUsers/hooks/useUserRights.ts | 9 ---- .../hankeUsers/hooks/useUserRightsForHanke.ts | 16 +++++++ .../hanke/hankeView/HankeViewContainer.tsx | 4 +- .../hanke/portfolio/HankePortfolio.test.tsx | 24 +++++++---- .../portfolio/HankePortfolioComponent.tsx | 25 ++++++----- src/domain/mocks/handlers.ts | 13 ++++++ 11 files changed, 150 insertions(+), 45 deletions(-) create mode 100644 src/domain/hanke/hankeUsers/UserRightsCheck.test.tsx create mode 100644 src/domain/hanke/hankeUsers/UserRightsCheck.tsx delete mode 100644 src/domain/hanke/hankeUsers/hooks/useUserRights.ts create mode 100644 src/domain/hanke/hankeUsers/hooks/useUserRightsForHanke.ts diff --git a/src/domain/hanke/accessRights/AccessRightsViewContainer.tsx b/src/domain/hanke/accessRights/AccessRightsViewContainer.tsx index 93bd3450a..cf5c08827 100644 --- a/src/domain/hanke/accessRights/AccessRightsViewContainer.tsx +++ b/src/domain/hanke/accessRights/AccessRightsViewContainer.tsx @@ -7,7 +7,7 @@ import ErrorLoadingText from '../../../common/components/errorLoadingText/ErrorL import { useHankeUsers } from '../hankeUsers/hooks/useHankeUsers'; import useHanke from '../hooks/useHanke'; import AccessRightsView from './AccessRightsView'; -import useSignedInUserRights from '../hankeUsers/hooks/useUserRights'; +import useUserRightsForHanke from '../hankeUsers/hooks/useUserRightsForHanke'; type Props = { hankeTunnus: string; @@ -17,7 +17,7 @@ function AccessRightsViewContainer({ hankeTunnus }: Props) { const { t } = useTranslation(); const { data: hankeUsers, isLoading, isError, error } = useHankeUsers(hankeTunnus); const { data: hankeData } = useHanke(hankeTunnus); - const { data: signedInUser } = useSignedInUserRights(hankeTunnus); + const { data: signedInUser } = useUserRightsForHanke(hankeTunnus); if (isLoading) { return ( diff --git a/src/domain/hanke/hankeUsers/UserRightsCheck.test.tsx b/src/domain/hanke/hankeUsers/UserRightsCheck.test.tsx new file mode 100644 index 000000000..b2a5f8b1c --- /dev/null +++ b/src/domain/hanke/hankeUsers/UserRightsCheck.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { rest } from 'msw'; +import { render, screen, waitFor } from '../../../testUtils/render'; +import { server } from '../../mocks/test-server'; +import { SignedInUser } from './hankeUser'; +import UserRightsCheck from './UserRightsCheck'; + +test('Should render children if user has required right', async () => { + render( + +

Children

+
, + ); + + await waitFor(() => { + expect(screen.getByText('Children')).toBeInTheDocument(); + }); +}); + +test('Should not render children if user does not have required right', async () => { + server.use( + rest.get('/api/hankkeet/:hankeTunnus/whoami', async (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + hankeKayttajaId: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + kayttooikeustaso: 'KATSELUOIKEUS', + kayttooikeudet: ['VIEW'], + }), + ); + }), + ); + + render( + +

Children

+
, + ); + + await waitFor(() => { + expect(screen.queryByText('Children')).not.toBeInTheDocument(); + }); +}); diff --git a/src/domain/hanke/hankeUsers/UserRightsCheck.tsx b/src/domain/hanke/hankeUsers/UserRightsCheck.tsx new file mode 100644 index 000000000..910576e64 --- /dev/null +++ b/src/domain/hanke/hankeUsers/UserRightsCheck.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import useUserRightsForHanke from './hooks/useUserRightsForHanke'; +import { Rights } from './hankeUser'; + +/** + * Check that user has required rights. + * If they have, render children. + */ +function UserRightsCheck({ + requiredRight, + hankeTunnus, + children, +}: { + /** User right that is required to render children */ + requiredRight: keyof typeof Rights; + /** hankeTunnus of the hanke that the right is required for */ + hankeTunnus: string; + children: React.ReactElement; +}) { + const { data: signedInUser } = useUserRightsForHanke(hankeTunnus); + + if (signedInUser?.kayttooikeudet.includes(requiredRight)) { + return children; + } + + return null; +} + +export default UserRightsCheck; diff --git a/src/domain/hanke/hankeUsers/hankeUser.ts b/src/domain/hanke/hankeUsers/hankeUser.ts index 81df645e6..fd18b105f 100644 --- a/src/domain/hanke/hankeUsers/hankeUser.ts +++ b/src/domain/hanke/hankeUsers/hankeUser.ts @@ -14,17 +14,19 @@ export type HankeUser = { tunnistautunut: boolean; }; -export type UserRights = Array< - | 'VIEW' - | 'MODIFY_VIEW_PERMISSIONS' - | 'EDIT' - | 'MODIFY_EDIT_PERMISSIONS' - | 'DELETE' - | 'MODIFY_DELETE_PERMISSIONS' - | 'EDIT_APPLICATIONS' - | 'MODIFY_APPLICATION_PERMISSIONS' - | 'RESEND_INVITATION' ->; +export enum Rights { + VIEW = 'VIEW', + MODIFY_VIEW_PERMISSIONS = 'MODIFY_VIEW_PERMISSIONS', + EDIT = 'EDIT', + MODIFY_EDIT_PERMISSIONS = 'MODIFY_EDIT_PERMISSIONS', + DELETE = 'DELETE', + MODIFY_DELETE_PERMISSIONS = 'MODIFY_DELETE_PERMISSIONS', + EDIT_APPLICATIONS = 'EDIT_APPLICATIONS', + MODIFY_APPLICATION_PERMISSIONS = 'MODIFY_APPLICATION_PERMISSIONS', + RESEND_INVITATION = 'RESEND_INVITATION', +} + +export type UserRights = Array; export type SignedInUser = { hankeKayttajaId: string; diff --git a/src/domain/hanke/hankeUsers/hankeUsersApi.ts b/src/domain/hanke/hankeUsers/hankeUsersApi.ts index 9fab334a9..5ae8bd20c 100644 --- a/src/domain/hanke/hankeUsers/hankeUsersApi.ts +++ b/src/domain/hanke/hankeUsers/hankeUsersApi.ts @@ -17,8 +17,8 @@ export async function updateHankeUsers({ return data; } -// Get user id and rights of the signed in user -export async function getSignedInUser(hankeTunnus?: string): Promise { +// Get user id and rights of the signed in user for a hanke +export async function getSignedInUserForHanke(hankeTunnus?: string): Promise { const { data } = await api.get(`hankkeet/${hankeTunnus}/whoami`); return data; } diff --git a/src/domain/hanke/hankeUsers/hooks/useUserRights.ts b/src/domain/hanke/hankeUsers/hooks/useUserRights.ts deleted file mode 100644 index 929a9a9b7..000000000 --- a/src/domain/hanke/hankeUsers/hooks/useUserRights.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useQuery } from 'react-query'; -import { SignedInUser } from './../hankeUser'; -import { getSignedInUser } from '../hankeUsersApi'; - -export default function useSignedInUserRights(hankeTunnus?: string) { - return useQuery(['signedInUser', hankeTunnus], () => getSignedInUser(hankeTunnus), { - enabled: Boolean(hankeTunnus), - }); -} diff --git a/src/domain/hanke/hankeUsers/hooks/useUserRightsForHanke.ts b/src/domain/hanke/hankeUsers/hooks/useUserRightsForHanke.ts new file mode 100644 index 000000000..a883f8407 --- /dev/null +++ b/src/domain/hanke/hankeUsers/hooks/useUserRightsForHanke.ts @@ -0,0 +1,16 @@ +import { useQuery } from 'react-query'; +import { SignedInUser } from '../hankeUser'; +import { getSignedInUserForHanke } from '../hankeUsersApi'; +import { useFeatureFlags } from '../../../../common/components/featureFlags/FeatureFlagsContext'; + +export default function useSignedInUserRightsForHanke(hankeTunnus?: string) { + const features = useFeatureFlags(); + + return useQuery( + ['signedInUser', hankeTunnus], + () => getSignedInUserForHanke(hankeTunnus), + { + enabled: Boolean(hankeTunnus) && features.accessRights, + }, + ); +} diff --git a/src/domain/hanke/hankeView/HankeViewContainer.tsx b/src/domain/hanke/hankeView/HankeViewContainer.tsx index 0951ab630..1f1346ec5 100644 --- a/src/domain/hanke/hankeView/HankeViewContainer.tsx +++ b/src/domain/hanke/hankeView/HankeViewContainer.tsx @@ -5,7 +5,7 @@ import { ROUTES } from '../../../common/types/route'; import HankeDelete from '../edit/components/HankeDelete'; import useHanke from '../hooks/useHanke'; import HankeView from './HankeView'; -import useSignedInUserRights from '../hankeUsers/hooks/useUserRights'; +import useUserRightsForHanke from '../hankeUsers/hooks/useUserRightsForHanke'; type Props = { hankeTunnus?: string; @@ -13,7 +13,7 @@ type Props = { const HankeViewContainer: React.FC = ({ hankeTunnus }) => { const { data: hankeData } = useHanke(hankeTunnus); - const { data: signedInUser } = useSignedInUserRights(hankeTunnus); + const { data: signedInUser } = useUserRightsForHanke(hankeTunnus); const getEditHankePath = useLinkPath(ROUTES.EDIT_HANKE); const getEditRightsPath = useLinkPath(ROUTES.ACCESS_RIGHTS); const navigate = useNavigate(); diff --git a/src/domain/hanke/portfolio/HankePortfolio.test.tsx b/src/domain/hanke/portfolio/HankePortfolio.test.tsx index 11628bc3f..a15336ba1 100644 --- a/src/domain/hanke/portfolio/HankePortfolio.test.tsx +++ b/src/domain/hanke/portfolio/HankePortfolio.test.tsx @@ -12,7 +12,7 @@ const endDateLabel = 'Ajanjakson loppu'; afterEach(cleanup); -jest.setTimeout(20000); +jest.setTimeout(30000); describe.only('HankePortfolio', () => { test('Changing search text filters correct number of projects', async () => { @@ -30,7 +30,7 @@ describe.only('HankePortfolio', () => { }); expect(screen.getByTestId('numberOfFilteredRows')).toHaveTextContent('0'); expect( - screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta') + screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta'), ).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: /tyhjennä hakuehdot/i })); @@ -50,7 +50,7 @@ describe.only('HankePortfolio', () => { changeFilterDate(startDateLabel, renderedComponent, '11.10.2022'); expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('0'); expect( - screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta') + screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta'), ).toBeInTheDocument(); changeFilterDate(startDateLabel, renderedComponent, null); }); @@ -61,7 +61,7 @@ describe.only('HankePortfolio', () => { changeFilterDate(endDateLabel, renderedComponent, '01.10.2022'); expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('0'); expect( - screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta') + screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta'), ).toBeInTheDocument(); changeFilterDate(endDateLabel, renderedComponent, '05.10.2022'); expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('2'); @@ -75,22 +75,22 @@ describe.only('HankePortfolio', () => { const renderedComponent = render(); expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('2'); await renderedComponent.user.click( - renderedComponent.getByRole('button', { name: 'Työn tyyppi' }) + renderedComponent.getByRole('button', { name: 'Työn tyyppi' }), ); await renderedComponent.user.click(renderedComponent.getByText('Sähkö')); renderedComponent.getByText('Hankevaiheet').click(); expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('0'); expect( - screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta') + screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta'), ).toBeInTheDocument(); await renderedComponent.user.click( - renderedComponent.getByRole('button', { name: 'Työn tyyppi' }) + renderedComponent.getByRole('button', { name: 'Työn tyyppi' }), ); await renderedComponent.user.click(renderedComponent.getByText('Viemäri')); renderedComponent.getByText('Hankevaiheet').click(); expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('1'); await renderedComponent.user.click( - renderedComponent.getByRole('button', { name: 'Työn tyyppi' }) + renderedComponent.getByRole('button', { name: 'Työn tyyppi' }), ); await renderedComponent.user.click(renderedComponent.getByText('Sadevesi')); renderedComponent.getByText('Hankevaiheet').click(); @@ -102,4 +102,12 @@ describe.only('HankePortfolio', () => { expect(screen.queryByText('Hankesalkussasi ei ole hankkeita')).toBeInTheDocument(); }); + + test('Should render edit hanke links for hankkeet that user has edit rights', async () => { + render(); + + await waitFor(() => { + expect(screen.queryAllByTestId('hankeEditLink')).toHaveLength(1); + }); + }); }); diff --git a/src/domain/hanke/portfolio/HankePortfolioComponent.tsx b/src/domain/hanke/portfolio/HankePortfolioComponent.tsx index 103c40abb..c16c344d7 100644 --- a/src/domain/hanke/portfolio/HankePortfolioComponent.tsx +++ b/src/domain/hanke/portfolio/HankePortfolioComponent.tsx @@ -39,6 +39,7 @@ import { SKIP_TO_ELEMENT_ID } from '../../../common/constants/constants'; import useHankeViewPath from '../hooks/useHankeViewPath'; import { useNavigateToApplicationList } from '../hooks/useNavigateToApplicationList'; import FeatureFlags from '../../../common/components/featureFlags/FeatureFlags'; +import UserRightsCheck from '../hankeUsers/UserRightsCheck'; type CustomAccordionProps = { hanke: HankeData; @@ -114,17 +115,19 @@ const CustomAccordion: React.FC> = - - - + + + + +