diff --git a/.gitignore b/.gitignore index d64d59e6f..aa727b817 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,10 @@ package-lock.json public/env-config.js public/test-env-config.js +# Generated Excel-sheet with translations and it's cache file +locale_export.xlsx +~$locale_export.xlsx + npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/README.md b/README.md index 394a1352b..bf7b6160c 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,20 @@ the identification method in the local environment, edit the `.env` -file. Then either rebuild the docker container or run `yarn update-runtime-env` as discussed above. -All cloud instances use Helsinki AD identification for now. +In the cloud instances, dev uses Helsinki AD identification while others use Suomi.fi. + +## Excel for translations + +You can export an Excel-file with current translations. This can then be sent to translators. + +1. In the repository root, run the export script with `yarn locales:export`. +2. The translations are written to `locale_export.xlsx`. + +After the translations are added to the Excel file, they can be imported back. + +1. Place the translated file inside repository root. It needs to named `locale_export.xlsx`. +2. Run the import script: `yarn locales:import`. +3. The translations in `/src/locales` are updated. ## API mocking diff --git a/public/helsinki.png b/public/helsinki.png new file mode 100644 index 000000000..e247c5af8 Binary files /dev/null and b/public/helsinki.png differ diff --git a/src/common/components/footer/Footer.tsx b/src/common/components/footer/Footer.tsx index b36c53a4d..058933a29 100644 --- a/src/common/components/footer/Footer.tsx +++ b/src/common/components/footer/Footer.tsx @@ -19,7 +19,7 @@ function HaitatonFooter() { - + ); } diff --git a/src/common/components/textInput/TextInput.tsx b/src/common/components/textInput/TextInput.tsx index 2471c4e01..a33823ec3 100644 --- a/src/common/components/textInput/TextInput.tsx +++ b/src/common/components/textInput/TextInput.tsx @@ -9,6 +9,7 @@ import { getInputErrorText } from '../../utils/form'; type PropTypes = { name: string; label?: string; + maxLength?: number | undefined; disabled?: boolean; required?: boolean; readOnly?: boolean; @@ -23,6 +24,7 @@ type PropTypes = { const TextInput: React.FC> = ({ name, label, + maxLength = undefined, disabled, tooltip, required, @@ -48,6 +50,7 @@ const TextInput: React.FC> = ({ className={className} label={label || t(`hankeForm:labels:${name}`)} value={value || ''} + maxLength={maxLength} helperText={helperText} placeholder={placeholder} errorText={getInputErrorText(t, error)} diff --git a/src/domain/application/applicationView/ApplicationView.test.tsx b/src/domain/application/applicationView/ApplicationView.test.tsx index 276f0339e..4bb709ad0 100644 --- a/src/domain/application/applicationView/ApplicationView.test.tsx +++ b/src/domain/application/applicationView/ApplicationView.test.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { rest } from 'msw'; -import { render, screen } from '../../../testUtils/render'; +import { render, screen, waitFor } from '../../../testUtils/render'; import ApplicationViewContainer from './ApplicationViewContainer'; import { waitForLoadingToFinish } from '../../../testUtils/helperFunctions'; import { server } from '../../mocks/test-server'; +import { SignedInUser } from '../../hanke/hankeUsers/hankeUser'; import * as applicationApi from '../utils'; test('Correct information about application should be displayed', async () => { @@ -57,7 +58,9 @@ test('Should show error notification if loading application fails', async () => test('Should be able to go editing application when editing is possible', async () => { const { user } = render(); - await waitForLoadingToFinish(); + await waitFor(() => screen.findByRole('button', { name: 'Muokkaa hakemusta' }), { + timeout: 4000, + }); await user.click(screen.getByRole('button', { name: 'Muokkaa hakemusta' })); expect(window.location.pathname).toBe('/fi/johtoselvityshakemus/4/muokkaa'); @@ -74,8 +77,7 @@ test('Application edit button should not be displayed when editing is not possib test('Should be able to cancel application if it is possible', async () => { const { user } = render(); - await waitForLoadingToFinish(); - + await waitFor(() => screen.findByRole('button', { name: 'Peru hakemus' }), { timeout: 4000 }); await user.click(screen.getByRole('button', { name: 'Peru hakemus' })); await user.click(screen.getByRole('button', { name: 'Vahvista' })); @@ -92,6 +94,28 @@ test('Should not be able to cancel application if it has moved to handling in Al expect(screen.queryByRole('button', { name: 'Peru hakemus' })).not.toBeInTheDocument(); }); +test('Should not show Edit application and Cancel application buttons if user does not have EDIT_APPLICATIONS permission', 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(); + + await waitForLoadingToFinish(); + + expect(screen.queryByRole('button', { name: 'Muokkaa hakemusta' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Peru hakemus' })).not.toBeInTheDocument(); +}); + test('Should not send multiple requests if clicking application cancel confirm button many times', async () => { server.use( rest.delete('/api/hakemukset/:id', async (req, res, ctx) => { @@ -103,6 +127,7 @@ test('Should not send multiple requests if clicking application cancel confirm b const { user } = render(); await waitForLoadingToFinish(); + await waitFor(() => screen.findByRole('button', { name: 'Peru hakemus' }), { timeout: 4000 }); await user.click(screen.getByRole('button', { name: 'Peru hakemus' })); const confirmCancelButton = screen.getByRole('button', { name: 'Vahvista' }); diff --git a/src/domain/application/applicationView/ApplicationView.tsx b/src/domain/application/applicationView/ApplicationView.tsx index 2066e6717..542c89730 100644 --- a/src/domain/application/applicationView/ApplicationView.tsx +++ b/src/domain/application/applicationView/ApplicationView.tsx @@ -38,6 +38,7 @@ import { ApplicationCancel } from '../components/ApplicationCancel'; import AttachmentSummary from '../components/AttachmentSummary'; import useAttachments from '../hooks/useAttachments'; import FeatureFlags from '../../../common/components/featureFlags/FeatureFlags'; +import UserRightsCheck from '../../hanke/hankeUsers/UserRightsCheck'; type Props = { application: Application; @@ -119,21 +120,25 @@ function ApplicationView({ application, hanke, onEditApplication }: Props) { {isPending ? ( - } - onClick={onEditApplication} - > - {t('hakemus:buttons:editApplication')} - + + } + onClick={onEditApplication} + > + {t('hakemus:buttons:editApplication')} + + ) : null} {hanke ? ( - } - /> + + } + /> + ) : null} 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/edit/HankeForm.test.tsx b/src/domain/hanke/edit/HankeForm.test.tsx index 77bf5b64e..2810ba916 100644 --- a/src/domain/hanke/edit/HankeForm.test.tsx +++ b/src/domain/hanke/edit/HankeForm.test.tsx @@ -119,6 +119,23 @@ describe('HankeForm', () => { expect(screen.getByTestId(FORMFIELD.KUVAUS)).toHaveValue(hankkeenKuvaus); }); + test('Hanke nimi should be limited to 100 characters and not exceed the limit with additional characters', async () => { + const { user } = render(); + const initialName = 'b'.repeat(90); + + fireEvent.change(screen.getByRole('textbox', { name: /hankkeen nimi/i }), { + target: { value: initialName }, + }); + + await user.type( + screen.getByRole('textbox', { name: /hankkeen nimi/i }), + 'additional_characters', + ); + + const result = screen.getByRole('textbox', { name: /hankkeen nimi/i }); + expect(result).toHaveValue(initialName.concat('additional')); + }); + test('Yhteystiedot can be filled', async () => { const { user } = await setupYhteystiedotPage(); diff --git a/src/domain/hanke/edit/HankeFormPerustiedot.tsx b/src/domain/hanke/edit/HankeFormPerustiedot.tsx index 789f2ae5c..de4546ee9 100644 --- a/src/domain/hanke/edit/HankeFormPerustiedot.tsx +++ b/src/domain/hanke/edit/HankeFormPerustiedot.tsx @@ -51,7 +51,7 @@ const HankeFormPerustiedot: React.FC> = ({ {t('form:requiredInstruction')} - + { + 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..a138d0936 --- /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/HankeView.test.tsx b/src/domain/hanke/hankeView/HankeView.test.tsx index db44e8c64..ec7c6a68b 100644 --- a/src/domain/hanke/hankeView/HankeView.test.tsx +++ b/src/domain/hanke/hankeView/HankeView.test.tsx @@ -4,6 +4,22 @@ import { render, screen } from '../../../testUtils/render'; import { waitForLoadingToFinish } from '../../../testUtils/helperFunctions'; import HankeViewContainer from './HankeViewContainer'; import { server } from '../../mocks/test-server'; +import { SignedInUser } from '../hankeUsers/hankeUser'; + +function getViewPermissionForUser() { + 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'], + }), + ); + }), + ); +} test('Draft state notification is rendered when hanke is in draft state', async () => { render(); @@ -168,3 +184,31 @@ test('Should navigate to application view when clicking the eye icon', async () expect(window.location.pathname).toBe('/fi/hakemus/2'); expect(screen.queryByText('Mannerheimintien kuopat')).toBeInTheDocument(); }); + +test('Should not show edit hanke button if user does not have EDIT permission', async () => { + getViewPermissionForUser(); + render(); + + await waitForLoadingToFinish(); + + expect(screen.queryByRole('button', { name: 'Muokkaa hanketta' })).not.toBeInTheDocument(); +}); + +test('Should not show add application button if user does not have EDIT_APPLICATIONS permission', async () => { + getViewPermissionForUser(); + render(); + + await waitForLoadingToFinish(); + + expect(screen.queryByRole('button', { name: 'Lisää hakemus' })).not.toBeInTheDocument(); +}); + +test('Should not show end hanke and remove hanke buttons if user does not have DELETE permission', async () => { + getViewPermissionForUser(); + render(); + + await waitForLoadingToFinish(); + + expect(screen.queryByRole('button', { name: 'Päätä hanke' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Peru hanke' })).not.toBeInTheDocument(); +}); diff --git a/src/domain/hanke/hankeView/HankeView.tsx b/src/domain/hanke/hankeView/HankeView.tsx index 6e1af6c87..503406998 100644 --- a/src/domain/hanke/hankeView/HankeView.tsx +++ b/src/domain/hanke/hankeView/HankeView.tsx @@ -53,6 +53,7 @@ import { import FeatureFlags from '../../../common/components/featureFlags/FeatureFlags'; import { useFeatureFlags } from '../../../common/components/featureFlags/FeatureFlagsContext'; import { SignedInUser } from '../hankeUsers/hankeUser'; +import UserRightsCheck from '../hankeUsers/UserRightsCheck'; type AreaProps = { area: HankeAlue; @@ -216,23 +217,27 @@ const HankeView: React.FC = ({ - } - theme="coat" - > - {t('hankeList:buttons:edit')} - - } - theme="coat" - onClick={addApplication} - disabled={!isHankeValid} - > - {t('hankeList:buttons:addApplication')} - + + } + theme="coat" + > + {t('hankeList:buttons:edit')} + + + + } + theme="coat" + onClick={addApplication} + disabled={!isHankeValid} + > + {t('hankeList:buttons:addApplication')} + + = ({ - } - theme="black" - disabled={!isHankeValid} - > - {t('hankeList:buttons:endHanke')} - + + } + theme="black" + disabled={!isHankeValid} + > + {t('hankeList:buttons:endHanke')} + + {!isLoading && isCancelPossible && ( - } - > - {t('hankeForm:cancelButton')} - + + } + > + {t('hankeForm:cancelButton')} + + )} 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> = - - - + + + + + diff --git a/src/domain/johtoselvitys/Contacts.tsx b/src/domain/johtoselvitys/Contacts.tsx index 5d2e9f0c8..ee2ca0a27 100644 --- a/src/domain/johtoselvitys/Contacts.tsx +++ b/src/domain/johtoselvitys/Contacts.tsx @@ -209,10 +209,14 @@ const ContactFields: React.FC<{ function fillWithOrdererInformation() { if (ordererInformation !== undefined) { - setValue(`applicationData.${customerType}.contacts.${index}`, { - ...ordererInformation, - orderer: false, - }); + setValue( + `applicationData.${customerType}.contacts.${index}`, + { + ...ordererInformation, + orderer: false, + }, + { shouldValidate: true, shouldDirty: true }, + ); } } diff --git a/src/domain/johtoselvitys/JohtoselvitysForm.test.tsx b/src/domain/johtoselvitys/JohtoselvitysForm.test.tsx index 9ad8d259c..512dec66b 100644 --- a/src/domain/johtoselvitys/JohtoselvitysForm.test.tsx +++ b/src/domain/johtoselvitys/JohtoselvitysForm.test.tsx @@ -617,4 +617,22 @@ test('Form is saved when contacts are filled with orderer information', async () expect(screen.queryByText(/hakemus tallennettu/i)).toBeInTheDocument(); expect(saveApplication).toHaveBeenCalledTimes(1); + + saveApplication.mockRestore(); +}); + +test('Form is saved when sub contacts are filled with orderer information', async () => { + const saveApplication = jest.spyOn(applicationApi, 'saveApplication'); + const { user } = render(); + + await user.click(screen.getByRole('button', { name: /yhteystiedot/i })); + await user.click( + screen.getByTestId('applicationData.contractorWithContacts.contacts.0.fillOwnInfoButton'), + ); + await user.click(screen.getByRole('button', { name: /edellinen/i })); + + expect(screen.queryByText(/hakemus tallennettu/i)).toBeInTheDocument(); + expect(saveApplication).toHaveBeenCalledTimes(1); + + saveApplication.mockRestore(); }); diff --git a/src/domain/mocks/handlers.ts b/src/domain/mocks/handlers.ts index aa01afc04..4a3408ad7 100644 --- a/src/domain/mocks/handlers.ts +++ b/src/domain/mocks/handlers.ts @@ -186,6 +186,19 @@ export const handlers = [ }), rest.get('/api/hankkeet/:hankeTunnus/whoami', async (req, res, ctx) => { + const { hankeTunnus } = req.params; + + if (hankeTunnus === 'SMTGEN2_1') { + return res( + ctx.status(200), + ctx.json({ + hankeKayttajaId: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + kayttooikeustaso: 'KATSELUOIKEUS', + kayttooikeudet: ['VIEW'], + }), + ); + } + return res( ctx.status(200), ctx.json({
{t('form:requiredInstruction')}
Children