From 3496cc84f94284c4eb491c38bae99987c409113c Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Fri, 6 Oct 2023 10:24:33 +0300 Subject: [PATCH 1/8] 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> = - - - + + + + + - + + + + + + - + + + {!isLoading && isCancelPossible && ( - + + + )} From a4b137172930ba1f8e1d576ff5ff5dd189a72b1a Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Fri, 6 Oct 2023 16:43:32 +0300 Subject: [PATCH 3/8] HAI-1947 Show buttons in application view according to user permissions (#381) Show Edit application and Cancel application buttons if user has Kaikki oikeudet, Hankkeen ja hakemuksen muokkaus or Hakemusasiointi permission for the hanke. --- .../applicationView/ApplicationView.test.tsx | 30 +++++++++++++++--- .../applicationView/ApplicationView.tsx | 31 +++++++++++-------- .../hanke/hankeUsers/UserRightsCheck.tsx | 2 +- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/domain/application/applicationView/ApplicationView.test.tsx b/src/domain/application/applicationView/ApplicationView.test.tsx index f445694a2..838975d73 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'; test('Correct information about application should be displayed', async () => { render(); @@ -56,7 +57,7 @@ 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' })); await user.click(screen.getByRole('button', { name: 'Muokkaa hakemusta' })); expect(window.location.pathname).toBe('/fi/johtoselvityshakemus/4/muokkaa'); @@ -73,8 +74,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' })); await user.click(screen.getByRole('button', { name: 'Peru hakemus' })); await user.click(screen.getByRole('button', { name: 'Vahvista' })); @@ -90,3 +90,25 @@ 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(); +}); 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 ? ( - + + + ) : null} {hanke ? ( - } - /> + + } + /> + ) : null} diff --git a/src/domain/hanke/hankeUsers/UserRightsCheck.tsx b/src/domain/hanke/hankeUsers/UserRightsCheck.tsx index 910576e64..a138d0936 100644 --- a/src/domain/hanke/hankeUsers/UserRightsCheck.tsx +++ b/src/domain/hanke/hankeUsers/UserRightsCheck.tsx @@ -14,7 +14,7 @@ function UserRightsCheck({ /** User right that is required to render children */ requiredRight: keyof typeof Rights; /** hankeTunnus of the hanke that the right is required for */ - hankeTunnus: string; + hankeTunnus?: string; children: React.ReactElement; }) { const { data: signedInUser } = useUserRightsForHanke(hankeTunnus); From 2c26416ef19effae55cb84adee2aba91504d74d2 Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Mon, 9 Oct 2023 12:14:12 +0300 Subject: [PATCH 4/8] HAI-1939 Fix problem where validation errors would remain when filling sub-contacts in cable report application (#382) If there were validation errors when user pressed Fill with own information button, those errors remained even when they should not. --- src/domain/johtoselvitys/Contacts.tsx | 12 ++++++++---- .../johtoselvitys/JohtoselvitysForm.test.tsx | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) 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(); }); From f0848ec2f0f0b9336f76146be945680935d7e9f5 Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Mon, 9 Oct 2023 14:59:28 +0300 Subject: [PATCH 5/8] HAI-1904 Add Helsinki logo to public folder (#387) Added Helsinki logo to public folder so it can be used in backend in the invitation emails. --- public/helsinki.png | Bin 0 -> 31152 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/helsinki.png diff --git a/public/helsinki.png b/public/helsinki.png new file mode 100644 index 0000000000000000000000000000000000000000..e247c5af8c81d5dd36314579600305d4951649b3 GIT binary patch literal 31152 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�Erh*vj&Tn~qEBRQZnmC)J zf*S}${CoUOIx_c!P-FO!1j_Ei!Y8cJac|I-4c8h8~#1GfsIL}!P9zf!36C-&K+s8#bBN>57Ja`nG5%)nip3{5a z1eO^6e_De%u%y*SrO|p0c+=Zm_Qs87rnY4z0vZirTR*RQ$*ITxpAVg)5rbP_Ycz61q4){ls8d!MRXYe13}Pr9w#Z{ zaJVxaAW1>SXYs4Iwj&3oLl5I3N;q<&$uWpZt%GV~m&|PBIln?C zkzx5RdzhiG%K~JGANB0~w$a;Bv=kW)W_m^sya5eT(+iupKm3=|)^5Z`t;e@t+*$rl zOhnnxf`xjkCB%K>hh;5?ccN^)S5Bxn>RnRf?>$UX>c>VU|nk}B9$+T3>Lk6WLJtFd?}(R8y(x=kE~uc&~f}^ zX!3KABPZxc6oVdfi072E({g zSyXzD1{!Sq@@eEp=WApXkv&IgwBIfcBres6=37(ul8(y~J$wAK@!nUJ%0BOX{8>mB z(hquk*wdGGTi!PQ{l4j4$N?o;(*}PRNdfTq#0^4xE41;72JAT?rF3ado2|h>mxwje zTNM=)mswB{sM*|q+CrVTc#Q6o1)60+-)G-fOZ-B>c;jZe<{fPb zV;1UrDTb(tKVfkLTd)j#6ky_6nY}zD6~S*De+=N{4u$r;Nn2OiwY;(jR^7F?#v8gk z%r%0g%_#vK5Svdn@QeSg1u$lIk`$OvJ-Mx`OkQt~_<6+hS8R{L%5*L9i(zp$Q-dI> z=EzN9*IH$!^A5XzUqB$4qN54x5KKA)5ST=QX!J-Dl5x`QR1wN zMyLjo*OMZiOoqSYY!SCQ1w?m%6tjZ{nDPyGd9X`^7+x?bAnLAv#|j9r$DMlHDORk# zBOeoRmwc3A@l#t*6N1QL#KmMw4URVwGx+bD);nWyh?(sk=e>L}WTK}))&5D4o)}xq~z^<0^k$?jrz&a zi_TH}@~Wz}mAbl)@U+w0cd5j?gDq?4`onVqt7}&u(T}qfl;pHN-0?3h%0CECTh|b# zxEuQ81WkY@ z^hU&N%T@&`mtQ+b^TaGoC4+syyqd{+Hl_#JrIGS+?i;r=`QN2LE{tY@eFyBi3QrEw zPTqco8|ag{g$|vcO_Tf*tMSFe)7*8G);I7Yw9|fz1Wwk?r*EY5ZXJ+{k~P+B<-f4h zp?H+GO2CX!@<}tDX`khfi+UQl4*AGLnQKzSrLg+mG-5 zG3nx7h=X$PnLmOB+F%}Uykc~qp^!GH)R*K0qH&{=8eUn$sy(VA^H6@RCBIH=v zy!mDn-e7O@;vHd4>6J&+CH+6ToHG}6ICb>ttCs{&T!Zbld1Gf{!7w@CbTF0l?Nfoy zeeQAFrRvkr<=jM?qRR#EH{EVWF?K}%MDJYlgkYUVB?Vn0s35Q9(V~2%lGDyHK9zTi z0^XImvXMeNkL_~3)eve@swE+^^Pm!hO?}6~VRJVd>XlbG2!e3?iWy{}_~#uH<@=4FEt*XypVV*v=@%x(4jd z?OE7)OESW{68psm{1mfb7_jLbwPr=oi}rlj+1sBWRWI|GE&;A8_9fQCT>8jfN59_= zsI1Re-Z!n{I(-0vL|&b6n@cWHqg+NpcMR_Yc6;_j_A}s(1m#M~vQ48my+UjQLsnBX zc$uDwxX=U4V2gAzBW-;y-Jm>tlJ-b_?~|vBTLrQuy^mIjqY-6O32AZJqTU0~8^8`8 zh$W{C*lifD+wJUtAc;ck5!&E=TXuTk$7nOlD*7??UYAP{zJ;rp5#L< zgnJW~Jr@g)DUIFl<6klRIg%CVdj431-!jA-X(Zjr4RDHuLYoC$RXUwV57nWjsQFL* znc1ev6e)rd_xN69&_y&?e1HHy7HM(13rguOZU1Beo)3eEDrLA@-n%rLqdzD z=oUU4fq>7u8t2LdDYw4&bO}EH%q3fPJaG_#Kn^&nr)e7L%w)A=QR3~in*tK;?5_?d zpLGj9hZE$|HI0(II8@kn{E%=sAA}&B+oHH1^sPlU=zf!Bt-C1GLkUgPvPv1`X>2fM z9IiT`ayaNNm%Cx=73J8#??3|kOg@)1)X6)qCFGf#vls`Eu`^DW{CLJ$n%xAS8@mp3 zES^(ckkcdc=IgzwHJZ~5kdK&oxVwwZjOTUP-L+`4TSon z$*@cP0o7YkWs9!wi6bdhaTybh$c1u%!=Lbst2-H53)Y0IQh>b`cE%9wky-)ht3nRQpx)C!B~DFUVc6O_%9#q1 z^Mcbi2|)|@fM!+CDB>}}e0L5_u6lrcsp zx$}*))$=>?!Y%7CK-Lrmuc4l{QO8sU?f~-?Q)LCf+TdrA zK2~{D^HdR6T~39VN2j#UU89v1C@#f*3l7dhK5erDM!`G-+z!_PzP&@#a_=Sxfw_^x zgu}rZZKlezr?|+5q9`KD*sjL$>H&W8N8h#Wn0^`j)8}OmMo(B=H|2cD=eVSJ;;^`% z;!2#ySx9g`QX;w+Sh@L6M*#v6$f|p1syxVl7%*^d`}y->TfT=ys0@!Io_$(o0eY!X z*Q~TSvT2Zhg2s4v^3%JNde+I9Cj`0-Ww?HJ(Vs>pEl34%hsHHoKQpI#b>kQTz z+{~U11G?sOr_f^wGekSfjX7QQ`pG^!DN0a|OMrnZNr;RGR^*P>cc2%DJ9vAEp}^R^iV$_!Yd?H(ZB$6x(2G2v_Lu^*dT%OAEn}&?yW|;N!rm( z_rG3sP`M5QnU^=?;OrYEV|SkYItyTQLE%bYnHr6L1!RAJJBhDmWfS>82d4fL%Xk~~ zWA5{?W(i2^t*DiORo*8$0CV-o!Lvk!a~+2&vfP$cp0$GG{O6+VBfXXDwUEz=P+;On zuLNXW%=M`q!>UrX)OqI9I6lz>81T>l0D}n!`Yd|z+iFG-cM0N&kjNv2(XRIeh*rW< z=fD)Oi!}TjhZub41D00okO~o-Sv@bNJ-)766rX-Wj|^(|{J>`nk(y>)vhbth#+*wq>6i1vJMMP93dP<0{QP!Sm_BaiS?pWrONm z8e7vjpI||P5(wcD+f})+`q-lu(IaapxS2-=w@gY{YM=NV5|E30YAfqb+GRz9zsely z0a>3lUs>LOrZ|L6HHvQ1@!#f(RDM-`QZDMLb?mFpIS4<}WdCD7Mo+&g}#RzUy9G&392D;ViGg3V9oC22k?dVfLp zz=KPx*K$SFRd8Cb$Nx*vsecSTs9Cx?x}BZhjn_O3Rb26m3Ot0fLpqU$x&*h(P%0s9 zJ&J=YKBt9InkhTNExFg^fED^9FH`7xE?xFR_0Q0fE9aZYp20?hpV4AqrtAtAjTueg z=IiAriJ&xudt-hAn1IoOivsf5p- zqN-M_d(Y0@B!-agBT3QY*#|^eEQ{B~r{;rf+M2g#8__5L!orp_6fW_Tx8n`!3U;`9l6t`DY+uwcWu;e&ay5v;0?8Yu`ysG5V?tksi((sXDGYfuI zN29NE2@}w8barqiubJ%K)ou23 z_ItX8!cb)d@)D2=pX54#=4;m-K_$FFlgkiZ1%YVNfl<`-nEy-vZqU>sen1AvC^wyj zyD&EpbQ~E4*i2or;s$|Hf}z651kh~oPsq0j9qac2C5vJ6Cx8eY(DeX=3J|t=ZPw5M z+o!;g0&Zh{%%9KdfVIBbzsN3lT$m60Hq(uB@c$1I{U_qo%gzk=D_X(^F#kDHX$e7# z!DBIq5Udc3*bh7$yNT&3K>e1NtmZkkI1{gU81M{EMhU}$T)-xRqrB!e@2f-F_!EZ2L0_8_7wX9QP!Ac3NPZ0}oi{1HDO>@|~#o znKfx)jvcMuJ#&&OTo{_s>7h3HCE`1kq6pio*;szEWBf)c%{o(vHn*3I^qiaJYO;Y@ zh;_x{!52DKYVT4U5R>To?i1k(_ zd1^KU4hZq)&nS*G#{0Fke6g=Xc`CnJxc+!tGS9YH+3&l1hE^;3C#9aRDb96<6c!#H zd2gwpN%A?B6gRP{Gud6LK)Z4r*Wm0HO7$7{<@LzaY$crr^X`!}?dA3BE4ZDX+cjYg zgspWAFKfJQwo5Q1PBOpTkpPR~pS!m6CBIj>7Zj%X!K6XD3PTTjGSFJGl+@7`{3O>jAAeLLa|A3%Zj!`7^P7e0+`9_eQg){XM)E z?oxI&?GplFbT}gg?0J=|h#%XseCRvMI~Or)qh4&NQ|a-YL3I&7h#U|B+mqIBun;V1 z&>qw->o{UZ*m(6arsm%gI^m!s4#W09Wc08S@hs8sj16zgRwuzSfud?n?G)y^K_7Im`4OwG{PwDUX)&2K|D4rk6-62hJ7D@a2O~`>g z+}#bSXJAP~v8=jpsM86`4C6zW4=lXArHpJ??`~TkS}u1%XmCu%YL1pAAlCV4^&SdcSF(jDnufRBS5=BOhw+pHt%VIBUuOMxUp@ zWaGmOaq)H&b~VpZtM$t;rLGMV2g=9W7T5O! z_b=HK?@*}{17MSXVrh-DQ!4|?(XYH$ymM{qI%*8pP2EQh_@6}8+UA|o^ffQfs8Afr zqG7^_^bO&Wlc_WbF)AdAuZ(vD@7|t}u37@;=hbL!_h6IvekY|4uG&q~#BT2OVNOMy z&lzlSlCJFo&0w|hA7N>k^1{#t&)f&wA2R=Z6(i{JSB_;km2K=_b9?Yer+v;Z=;R%$ zY!JC9QMEDs5eaI9ligl@@FnmheCjVXAHD@ri3)tXpmop@Gye9kACvG}jF-3VvcMFJ z$Ei0t?PX(POS^uzKIAt#jD=g=Sspj`%6%|Y2?~6=;a-(v5!)d|8z*R$TvLF}UP$%i(VXA2Aimc|^R%haFhYYHg-pT#Ea+$ONCN8lbcf63_m=TZr@{BPggpz4lovz)W z5(d82cC6csquAcNiLbw1<<&Axt&`tO4PN{zSbtY8(U-lHZ{1XrRC~Qq{>~=IH784M zQ!J{Y`xUP~_(Yjm*KvEiiyC_}9NFNCp<>Tqmnzu#&YCmVN4ShpHKIk6$E&?Z8;8zk z*GVM*l(X|bB>wfCQkXPlQ(h98p=ivtulYq1dUi|7q?NA;pyj^|*8P;Y!nV*y7#%pSY zb8`c=UxLk^HlKdUC3tdQL%NCzn>E!2c76G$9Ckv{w>94a;J`acI!%>ul$nFmBE)Yd%w&v_n z*uo!y;6ukOAy}eoo)24h4HP!U3FrQj@7+xC;%N~L^fV-!vzu;XdouHllO8JisysXf zGHEqYaA?1(&*L8vZ}T{3_1-avWa25xOI(?A>6FOM4-<$UfiLrhDkWm}l&Awi>LDkDpw{-GDs9yjD3vFIUmP$Kk%Xea| zU9!40>N-hUg%rtM$*KQqw?+)}WJXRl%DPb#pHvC9*el*?nNd%ZGVF?PHM;3ErLg)j zt6*2;W!@rcAH3z*DKI+&1F@iulyCoGG51Rj;g%arl>*Q^MCF@TCqQNAa`C{-%7eI| zs*yGkGpg$Z?pbfqT8d;u(2{KR0}aQh`ax^KH@FQD-c-TqpiD&rsv{aD(sVm{ad9`w z$Md|6`7atR!Yra!vNNA@61z5k*~dJ&>jsWAY~frd(*}rveB7-Ks5SIfCr#~`B!;O{l1I8X8@c(J53&MDc$rb;gMz0#8?$U%MWf8ELkxPf|lEXY^#(eRm3%7R1iS#XaR86V^%(CozWxi{gsq=si`_p%%7|kdt)7c74f&x~U+!3islCkQoW8k*s%?s*@N4(+w4Pf2W>%3_dej<HuiOXeb~t|>b;Piq{ zQUkFyA2#W>p(-@>nN3tUT$Q>dex;inzxQc2XE7U3sf#s|3RIm8Q&t_H+8<%XNn|dTwcj^~sW-Cod?jQN%(N33e z`$IouxMQ@GS4d^yYDQZ4D?5d%cI;+u&i;eFceP#B+ z_l(4A{;qYe+X~x$D6J(L=r37*^>FNXUnYsaB0v6JsY$1{pj)Zsu@-z$<##xyE@&<& zIH}1;DnSdq5`f@@ffb!(E9%9`Un#74FZ2@Ap92D_!OqlqGY!#GflWs&UAoNpa~V>9sUY z7*6%Lc}~nb%D76Yk5~oASzI&Hj5*D0yKxv*`6iJ6c=v8A)u1tz;2-3sRO1`3$SwCQ zefegJp_4h%a5$=+5^y21`@z;cVOMi1n(JJ-)B|Ki?C|$1uBQBPNs+ z_X7BH0}(gy)aI=x;rjJ8=2!d`TXXytYuGrcHPcBh$~@$<1_F0DpLmsG!xG0U)!F1p zS$>YKMvFL&!^t@r^8r6%HHe1%^a6~Wuwy$Y{h(eQVS+ZCTMCXE;(HGp95|l*MrHKk z&0hhPoiC_Dp`A)CjbF9`CXFV!RHQN~GZAIM;-0e2X2F@Wkp~x?f4dGtUy@{>tVfE~ zJa%d%m$3-#Qyi9`?0@dLs`g}X{~JYvcZ98oil+4LiWs*x(&SVR>Y|{ZcWt~8!?!a% zkyCg9UJw2<)6sP5vIALmYw)X^zO?>`rp>SY&Cy6fHRZiV>uAiR~p@!RB9SoO- z(ymRh-2eHWqaR9x-*TGFp-y;lPyD`}Nwda_yKKpgj5MCB#eEHb7?>GHx-p?_&4=HF z23A)6$v@b+{kH$|M<)J=m-<7!=`ah$&3hN9N*cHH z^`c)EJBMZE$Q$bI-{I7l2|vrF9TV81K}HTcpIsWIQrSR_&>eLw#b*3j+0VuwBj#+I z-?_CZE`Ob2l~sX`p>Lpp&ep6^72d!bl|_W05z|~c?KFnMIKQuZj*Z%E#!jSJeLYr zAM#6MJnL!-^#cySUd?PnqZ*QH<|E-V6XQt;@Jht!cmtl>2xwr1u5prRE zzd~ul-&1APv8H>|WXl}6o-8XOByX^v>a6cry(wXy>B?|LM*9ahXT#;!J>w^gKH;ca zJpNC1FLhr$*^?AkH`b`PR))!(tj7K_n*WVCC%-F3!L5bV5^}Ms@@gDBY0&qh7kAPA zvVG5+vK#V=Hl;B3IVC&S3}%_0k_j#4dB5HFrTVq>XFq{%d~%PG`FJf+ghp{H=%B4s z-J~`_<(l+sS0=S>E@V_WQr@N=Xu1<${w!2Qqg7^?S6>Av>|P-i(Ltwb!V?)-sygI8 zy3yjw z;Vszr%p3K+y23W~WBbbHs^4`7!zt+B{EMG|^|cT6w^EnCR**I^wS>_rWiB-#W66sb zqoR)CIc!jT{*~lT32y)EJ0&vVE`u60ZRMb;O8$`}qqg=+yi`(m#Nrt5~4V%bMl+L|s$yq%V z{I&h=skPhUS3FckS(>5DV?8-Pw|##_hHLGo<~{Og$lfm8oDTae+86h6uNLWXW5q{I z}sYi-#78D27W$Dl8fYvQ6i&ES$F46F5E^_Bg z)EC$D55U#ffre594OJX*@@A4CXA;b9=53+h8QPAM#kCz@{v1jJ$CT~IL3Y}rw21f> z^rjq^m(31BRq0;Nh)=YU5AMAt+n)D|*Cb}IK~;ZE>*Et)+Z)YcrJ3j7OIje-hOc4w($rJ_w&Q7IRX=DG!I@l*Ubg4$|~Y0sOs!a>n4q*elC9bc8q*YQY>l`=P}NHW+$*UZ>z&D!sx!AD8CVK^_R2U8+j7b08(&Y}lmM->rsNc0uc0RACbA{<1|(nBY;0 zXzF!pc@QQM-=)Qu7QCo&xL$+wXn+)0j$x&^I+ZPrGLsDFtA~FYSk{%1ae+U^!&V)l2waBZP>R+<{zV6%3%E*AI+}$D}GPjB^Y+VP_I9Fn=M;kkI&CNC8cS>|7Neg z4?Fj!X&PlXhfggU8ef+RP*Z9~@zl8tXlxz6T5Efp`sOlW+2eQxxa$Sptxy-IAQcd| z4J+WM?)E07kOMBJiZdLRQTCNb9=sQhUpaC7K*}|VtM{(r_{kw?Jf08!{p4nXErn4o zQY>N{vmb`K{8wMRyGEPy+dDt8Cs}H3cZBkY584T`zf&>#)Cawe`_&86x3@Zrf*m%3Q{`3XT^vhq-mYYueOL~V^t~dHPCH15;IBlYH zulBrb?qv^NYJ+{Wj87w1@;)iqyg5U1JeYN|`KWpxDRnEr&-1$L&91w5)OLDXoA4im zvB-03Qrj#G-;irgd4GIY0+sb^752fVn6EZF`+VrbIvr|62c~SHOjo$Dy2p3_4s5_m z-eAJRZ8>aJG`B1IhcDN+v->hFO3mC+?Ojvp8w>D|N-wsDtjigm<_2phCG9?J;LyKKgfPlt zVY1+Sui3>7Y>JyF7|!e0e*jVP?!n|?C-thI&q50#Kyuw%TCGoM_2=MG-SKmnYhPA7 zAQ)|d(erQ#)or``gB!T^LU3Up=dib6i(-(Fe3EzDlTaLWrS_Ox+xnH@oFc1+#dy4> z9@xW()fKki_qR=O-A6t((J@ap=1ESt?9dCveG3F(sR>D=v*hNLM%CcP$M=75-t#-w z+&W}Jd0Aqw4ToL~UiCzEAX7evPQ4>k_t7UgH=+IpkfBG)IPpX)x22od^Q47ir+Q;(HB8ri4z z%dV8+w%A_lGc%Nckg}Pj|IOwT;e2fs&X^_!qLE43ZE3>SD2JaLkDC^;EHbk%V6HmZ zFciOE`agt9KKA!B8UelT`2GzP&}Th}{&Agt!}z1+m%o97MOSpS-5lDKz7qIIdBF}q z-@7N8efeN>W>W*jE>vp7&zl<<=HYtXAsqZI4+1RmP%l$`HD~lC1r}IJ(z1u6$T|W_ zU9gYREr5*AgQnz{*7r*`QDs|C@2?pJ-wPN#eaa2UxSJZi%0Tq39`;9doTrruF0inq zXA3Os8`$G&o-^7-02u#n6OMQX&$SOZP`&pnCnkl-%-Rc_!=O{3r%T(DU7|U+~Rd)o^RWoaC7P1x*NQ%H^5Ezs40SQ7wsi6)oRTg**Kc$MVUvF-7A)EQ zaJ~JVs+>n55hhl)p>BUF3Ui4s-!64pC{@)4Wijk=gEJpf6i?fhy02`M_k`dVlLcen z+9#N+wC;@hYj}VC_4}H8SWj4V6f@NRrJo^%l>@);@5T%-v%AB7)_tT3af%6p?i{id zv=?r={gpvf3@1%UR!j>UopqeP7@EH5tuXdk$$`!ncYW-*xoDKh@&DE3rZWJa*Ad_cSv)4b2b3DGb!#V=*eBJBY1doqO0)B+WuZL z&k63h{7L=R>+X`kG4^{^IaJe+>*#C_>gXQ6trpusI-&4hWxu`157x%#u9^)_bb#XUXRaIH#{Gel0p}Z2wFNh{~b|=Vs`%F9J4evjh|*|Oi>9bpU#*a z-Gi3p!}Dm*3U>NBTr0R@K|bFOybPMK{;b&I9ZSb4sdam2kLSKfFmE$fXyG5{EVwUT zC+mB6cru?gJbLGI$_vKAABb&#)*s>-RuY{O#zPo?EEZ2}owZoy>-{-(Ry7IcE>TBh z{(Mj9&w7Dbd^di>M@*J@%Rve;$AoVgFIwZ04{p9=USfw&(!QxXf2~%Fs+4vWs`1sF zN!ie}H71-a*Vpnxe$(Tt-|C+WGWycgZemWks|rlI_GN8$hGeU#MGv=`y5=Sngq!#w zi;*nv8mjD8!sNJcl3>K@AsM9%nMu;iLCt#c95yssymBbytLJj_veW1dttnV1FF&EA`}3Uq$raHlnb)*)c3Mt+MNQfE#W3Q+ zx&bz4)qm>>ACcDdnMS5R*E+BkfR_Vq(Xiq?lE zr;mRR?nt;^b(n;xeA7didRJv!7)X`>t7!JF%q^`U*UEwW4rhm4>7ExZDU9g=)4d-V zpKjiKSMcu2E9yDp%-M2xngOA{47RkJUbZb#ODz17=favRHx)vw!noLSEcRw-`<2DL)CoIXOa=p+n-C*zM{raXSg*+%S`mEE7egf(XGNv~Lr3-7 z@tzH>>AxG`we);+IVX#I|M?Q>=U4NRbjy`p!WUN;^nCB!7#GCyI$L$#cv2Br$9XKl zkxvuK6)Eq_+q%kW_YpuhZ1j43-e2$j-E|bo;Bg9|1sLqpi=UMeQu#a zaAZ?fPFpFx!l`mSg`vkpr_p*3#|O^eId8)J>ucp?9~FfRd0pFxiO>;TPq z8t5nYwi<|Y_AkB898aI<-_w=@$0nE;DNCNJ&JL%l8m?c-r1C~fIy3~tS5L+AbOMtY zmAaS#3{C?hTM5^2)L1_AA$8|X7)zv=ZLb~GP-XY6OST4f{d~zP5t*WqYfnccYT)@y z&C>qn$1&$3e^exf`N5=0x|MZKIG;j3~K69bKd z0YB?c>QF<1JtrgvZ64C2s&`cVX+!3-E>Y2nNZ3B{ykYq0bjwU$ zq|>Z{{p%ew+m0IU{@Gdqy^T;o0d`)D7a{RWzj?FASE;nd`$IP!`%kxbarWpr=_y$w z!bGc%oVc>XYwTRPXjtIsv8v$2VTn63C6kWNxkTATS@!e!OwHa7Vnl%y5 zA-;`LXsnCbNB3Zk>GhvpE?)KBzi;c&VND;YUGS~8J9R@_hiW=&$pk5n!oNl1dV~QY z#V9Y;Q-+A^jGh)wf$A~ADEvG6VFfuRcUoV z=aPGQ&m&oX_nUxq-&M|D+O?W)o~0msExO3*9L&wmKEH6+!T8Sdn(6`9NE()UyPl=2 z7M-uvJL>vvzs3lyE_ASx+>bE%ZuODZ@%4jhE3w1+Oa%x}*Q2yB2Pn*Yq$Q!QXBZ%z z@))n~Wp9Ng>k>E?p9#A~c=4UGYwXmW(&x@&v>mkLLmxEtwNEMXf=S+7)8$iuEru1m z63+C>BAAlB-smfuXw7k#QjD$k6_;z&Zv^?Ylyx-1r{2{j=M7Y?i2mmz+5a98el4NW zjl)Sj+Ff%;x>y5vKmKyzTxc;1vtwJR6qI$){&puA-JTHWyN;uA8~!Ve@EOo`uv6`F zIrDibKvXZta)GX|Q`2@wclSZzp^t~S9Vp=<^JaIt_9{B!>Fht3#n?rKbO>$EEY<4( zC%bLd`Ca~SzX)3+_@cJ_8fxwy z#H7VP>70lYSsBZGu$5%F6p>!^g@Pds^skR4O{}~_9^azQ53`Bn(6wrEhi&@`6W&d) z>ecN{S#sU~4Si25w!fn>RADMihCr}wQQg#MGUwacrbL%V@ZKT)Aa=)DVxF@72kRUR z^BUmj%@Von%9QLZ0)-A+%~F@1;2jo%SF3l)O}r`&S@Q2L3S9Vs(bLlbiG2pB;AHQAf2ioZ(bfb6 zq&3Z^b(J#{;!oFO!Gr_=e~+WvI65ts+Ey9dH*+`fMhB z_s(_cn)Fn@9VF_nj=N{7+*Nl`)rfd1nZ?}Lci(^Q#`_ZK1PV}qMH$dRdUS(f-g7V*hv63{5G@4sDyBc2ws4%WN$p_AS^XyPVK$Wjw=ox&B* zP;=j@^rYn;?J1C$LcJX(hO%FjXtRZGB|HWr^^T@GRsCW zZ`Zfmo4F~$G7M+!Ewa*|*4A|#$@UgCNv)KPDz<}6zf@Kwvx2i2truAjP6iLmyqNac zeH2g&wedveyIWav7YkRyhrDl?x%t@XFKhvKUSrvSdneRG)ACIutKMkh=^(8Z>SiSN zTpnn#x(#9GMdzwgoi~*QtpinyT#f@x^T&fjKR+OY@wYaDur;dr=0`)wxeGTgb)qF` zNVlR3#wzWq1OF5~OK+0e@V*?S` z=Gy4^Ig0!8GL4FP?f4(xI*k_J-12~vv<=;rd+Ul&lIRjf*cE?H{u`svy}H`B*b{dM zag}3%3S*ztOwP?EF7qlxI~-Huo4zU9=2pIDj}BfraZnOSI%(AJ{zuog^5pP?QjHP* z5tTXBb$?{*_elXDzTLO@Knhpo@Om`y5gGvm^D+>v3zWP5R-U@w)KLm^{TM9l?c;|Z z!HDi-_4aQftwlCDT#fP9b1NcEE*XZM@@x(wsDal$%JffMmGiDr%6%ajMQN&&ImOi- z&yd?*C6JYIL7TG$*|i#@5+e{bsz0n*gaM`>Ot{I`y0$@;7ecN zJf0XN?O5&#s8d)Eze-a3Lv3>X4mWbn{@Sc=nZ!mY%wz!l_3lRuF3V;sbh5asQnOh6 z$HX99>-kLH8Nl_^XB$KuB9MOP8lhZjSmuK6`zrdb|fASG3uHJkP$8s;E8M^p5;dmoh9% z(tnM{4|k@DT)yjLzr5!W>{Sz+^mb94ZAPgLuox`b^5U#j_l2BCyFFp8+zoV=`+jPV zZzNerEqMxepNyRJ9LzWNGgFzQ8-~RnRqz}96msP12%0O`@Sa-Z7xZ8?(cH{fK_WN7 z8Mh@H>iqBOG_aFzX@0F@4?ZMOqj?*IjRaemevF_|xOUN@KLO}=A=O|^{3NVXmeiE? z$AC$Iy~!EP-WzN0R}^zC936wMdFr+;_X!QaLr*7a9CnyCQYbZV2F{ga1{pz<3w3we zol{7;)mXQrH+RH;1fB6Ly_#1#;pmpJ##87*pQhaXavYFmxNgYJl_xBG0Od!qPPM)K z=F|Kale|`*N4e1y1_$#jjEQ=%d=s0aJ#H{cV+*Xi7dY+@Fj8pBO$b$RPGsmK?D7j# zTtZaK&S*_m1(Obl#^w%<+QJ!bqlZwd2AcZI?5B=%cvjVI5atmesfJVo+tEj-8rwZ& zTJK%su?!Q@8fGIBJD>erPv?xjr-td?FShnpl4Oq>?pR5W(BA5eFO8Izq{`Q*cGfL` zn9IcFTtr`*bkIO;Ux@IGy?g2HKn!>5rEl@j2GX)K5Yb<(27g7?E#o`KR5o-akNS zs>))YI4ylSP=yo=XCERmVKOZLh!?NX#-5MSow>F=+v56jN#|PBe4XBp(h3ypwm6*s zjt*4EAA;w}O!F49pPtyA&TP5II*bMEtj7oPPm4!<#48J(gmqecww13A@?U)|HQg1L zHG0J+wwqg&%*6P)Y3$8S=!y%~V>aFGzrf*!k$hXx8cu3$%+bHav!(Irq*Dh-;zF%w zN8Z|gfm(PIegA}-EZ^wUd1_YD4UHv{I{GUpon-hTTjJ#a1r3`gq+sH}Rt&Hvb(bPhX-C!yMR} zB4=TS&yk-B+QmB4tC6F7PX5@?Tr;2HeDU1c$oc|lX7b*zWnsp{7L~iDx=V7=hj*ut zTMR>lUxRyB_cgSa0+dZqs8~O4#Si<4)pdu^oWQ2{TcN#^Dw`h#gF$W1;x@mbwefr` zsG|kdC;0}9Pl@MBcbfA0L~i~3)U(`Q+x3g^waIh|mH=7euECr~VfWMqS~PW8 z?_`ERywp@sFGmgV8BlP*W^T%#20YPu|8{U? zxjT|sCdyDGOCDT#fpie|gWPaYSYW!(wWf=HXTDDqwR(Zd0^n?bnlXJ=%($3~Z)SL( zuhN5^QG$>oPzOP$pCmDwRTL;NL;%`7DA!V$;D=+l!LkA)e-0x2bUsH@QOPbEklN*K z{MTR1Fdu*uDx^tr6$~FQbl(rfCh}KJP24ql*_pF*>v#Hk;Xg@h5Ok=A|BisZe+8jN zBmVpk#jM}M&|_xa+P2Ck%YvcOL5S zD7>hAXYCy2UBaoXe@1Ev({|2PXXOxnuvrr*@;T6tz*Kd@HR}7I zdhz`k%sO0E#8#qs40&swJOXwZ1+Po@&tk|^ zz{6;>-Mr2I*Kykv1wwBeb@3AEzG%3-_?w+jkk@3nq!j{0zFfuuMw9D>ffk;+ri-;5 zJ++*GnCopoQ;!madWyTm&yL*0G4E9l_t=0%bT25+lwVhPSH~X1;I7A9-vOc312Wm? zro?C$^{246RKTh&|3oEY%Gv3Yvvnt6oh#FN1?5HigrE;sE?R}qWxe6V^qU{SeYT#u zh~dw8IQjAj+M4=gF4b-jsy`oP7DB)=Zjzt1em83ci=-+2ivs|!we6QmOZoUE6*17R z9vmCQ)7`XKi^aPg%`XcS%}j+sW0P{K0eza$6tKs}f^Hv-XC%Y7fFn(|Mj&!`*)zJW zEJc3h)Q@2FwP@rmtB z^beoAbJNyhe0T2--FWJ_-QDck#;Hrg)eve)GOaec=IOGP4nPNBYY0B}y3SHA4x zjREc@x`^x)ZkW>aFa#&G)nU*UtqUHXb-X=2w!@@cfb3Q;J`gB6f;o4c``0d zwp!f)?5TIJ;VIS1@UA>XaC7gpr-PIww$ZPg8hAB0!QlCMEu!iI+zqFCKY84(j(Isl8F_3 zIs~8ilK=w`*Uc-94#p*zPk*JHa=IlSj{WAN!a3qb0HZwT@C3Vd{a5bt^4h}aNPmLtX+ zDV$!~2A{2)n}CO_O7mWtL&WT!^hQJcd;dR?sB|9r~B53t_O8V^RLOJ5W|20OP_I1)O(ql*G zQa9N8N@I$Yg575~Ks2RUdV84;St^WaQnt=~P^bjjb~`u5)qV|@6c;-@puYXw?Gy3& zY}H05dCmM08-W82hwWQ`fP2qCn(F5!SoBTOL2M#$y&AjFyO%|Z`)VpE#Um!1(n#DM zYt(fkEn_Ll97k2Q7Oz^Vjv$$flU)6~wr|oqT%CyXYO-3QIvR$Mm2z8K<@tRgxy+?f zRjJ+=gz-ZC{s#1DjE#I~M}6|zRnLr6)HYO(h0)EtxQA>^;mVrl*2hxc&HDz&9N#qq zz6!WbyJW$fUARA*d~FzmV_@UL4_o}&2=tGufx);<5QyOQVdsDD1(c^qiM~vVRRmH6 zAl7w%U&C3orpz(=l}&L$eQ9vYBX=N6z*Hq9_ItOCu{W>Sd%NS{xe2520{E4<@Zv0w zLSuix2mx9R{>kMHewX8xQp4DVTa)R3(zvSC9+NS)X+HcGkoHe=nbkoyycW;$bnom- zMqe{ZP2WZh^;;PM1E2WP6cx^{z!v0E8k31;i?5!m0-3`=mu_tL54TnrgHrc>y&{7dmrNE!&O^tDvl>6I0(Ts%lio+_Cx4C&Cgx=oKm z3o4zK!SRd(-W9Nf`MJ_5t2oRQFw5Sq|Hkh;e+P2{pC#4*K3KGH&P=AKp>}Ig9;EE_ zB$MBk8-;Dti_Up^vr}S)_&XCE%%iX%nuinxi^;o4JF_}ges-IPL#hn=jDyH%lM?OF zOOJv(y%NEG{p)F)mcvH^*;^TDAs4M5q^~wO9;CI)Ea{&?jb*@NNISRRmlLTv*=^u; zvX?tKJ;!(sRzLE^>u-uIb~*L*b#Z?GAr|oYGvV!014LLRZa!kQXRCNvk!1zE-u%bm z9e;bv)nYM50Q)}x`|{pQ#3BL3Osodn9NhF*$hDDMul{ISi}W-1tV1`ehOQ=TRd3C= zTxJixVqM&ZacAC00c8IIg(W6mt`;wE8Nz<>!;hSt4lniI_Xi(L{f(6`AGf@Ly5LzT z-@j(ip1C2s;k0OwBM4FxIs>-pO~fK=#_lZf$&xqsm23Z+ED$6((Krz*KU!Y)tA#23 z<*1@}Q9}wGEM`j~hMr%c-g~Gj$#?|Muq_Pq`EPqT(b>o#)vmx#Za+oKsXr?sdP@OB zz;JPNDVeJ7+yvn+Y5tf}YM#Suv4bup+P$|Qu^ay?>huW>#}TKjKk%PICT)heL2KaH z>MHf-l|%@%gpP7J@#f6~W!too=K`n9Ib8{5xc2iNl~W7gEGd`?VwL7P zcX+?CgW=qV43ejWTk-MiVZnhOlEm?Sx= z2bdR5iL2J>a9h8Ke53>)9aNE<;Fsj|{4T@Z*P}4MmiNrHc5@7~7@~(;Ur?l!AUn60 zO}!w&FOxw(e5Hh=hi*Qo-@hWnHxxGYaG{HTQ{G{>Pdupd4j2K+V9EDb-^>zR9P2(6jFRwoTQ_lM?yF=BmZ_AdzLMF0Mo7AWeQMEg#l!DN#iDI;3x`Hpj%4I%pmavHk3NG2G4mC?-iI zROjp!1n;_0_4GmZ_UMq7eC|b#)#%0G8R6egO27Jqb`;9Y^{mjdWFy)<3x%VP@f*$` z%1yXg^KNe97*7&tuu(ojT(7U9EQ#Xq(<2!n2@F6|AnrQ-eVZSVtQ5vxC-?zh!C`C( zn)<0P`hjJL?@Uiitbc9?vd-9Q!TXvqAL>x)h9zYJK{I2V-~!(6bKA?>Fodlg`kb+{ zGiVNjYk?<=fx3Bs*7`2L;^^42A1j1(nYT>1h|#zrz~Slfw>ApBfOioN?xCL&@Nuig(xKg3$ zwl9kfB628muRlHv)<2q;wrp1OnQeiK0Rneels|~Bt3VqH@~dZHBv6p;wKmSy=k?W)tKa zd&L@5<`@I)3py0v@j+Mlk#l=U(^&W?{&X}oG3~Q>c+-?vACsIZr*-Nd`v5!xIKd>x zB4CaH#Rrf{mVy;cdlSrUAIGQWz7&X@Dk!fR_n*`tGSh8_hRrr$k+vz`>uiLJ{fJmr@jQfzyTbd4^o_y`J1=HwH`fWU;f~A z(in;y9%PLE(`TGG!Tca=KZ>BIjDAch5WM|nJDZahvZ-I!b zXT=7XFl7YLG3Px&R0MZ&@`o5r>K+grC;TO#Y^}<5&mEg>&rB4>vt+emc7nnLfZXVu~yDbJ>x$zpx0#ILE?ZkCe&D4NT;BFJL%K}}X z!f$B`t3^aq9&vHgr45}V2R=e#JSHeJs<6^7B5 zmCMX87`NQF15(ptPwl;+vJY&@xq5xGJz6_L!dO^=LJPu z2x>GP;W);g=-3abMN}f%3LTdct3Znaja^G2Uk*YJdqvJJDD$80sl@2is{KKKjFlyl znR2&eo`7;43Yu>|2ZOv&U}A!oBy@1UAG-00b3tR@k|AJ6v4fbJKx-Ax!r&202$Brw zYAO)ExKMusH0jXzed)#kFzj0Ax>odGw3=Qbi+^**Nd&2am8wG~PufZTcct;O7!65! zsfk={b^|4msHT8Vx%ny@DFAD4aKs#nT&PC+{TWl}D}NR+yT1L6p*8P(xd*@=Oxc2_ zZ~9(Lc7cJTG^YL&FUsrl@O`#+;}e>xA-&`l3|7{XJ*DWWa^?5nHk{HcB-oRlk#7Y7 z-o{1yn?y#O_OLPrfpTUX-ahycAa66pg~n(c1=2J?*qrC3AppfTa^QexCcf;sOH>-J zOHRji1w3Ju8b*MLQ}{wFYNWXoxYzrV!P_HQm>LfsW#iSur;S5cTseb9K#?}P39EAj zH4??gWufye)LA27Ov_da{g)w26q4RlS=TjA-lPK3&GvrqFPdFfLwNNL{9^#aDRo)g zWl{#AJ*=dl%k2NAf#31gB?w9*zD(^#S)m{AUzr9kXE`lnlUFcgC;4>MFr>1^u}11e z;4WZUqb+nHsNvTknNbth0jYME4(x>L@A%+s2v~Sk(}hoH6YYZ91uaU`r8UH|0H}Q0 z3~t}B3Rhi#0fe`?3>Z#5X#ierY*k%nRDv6jQQgB*nE6~p7^S#Iz`+Z+g0TK&iMy0J z8k;J3;@S5QWbL1>@+2c5u`igbPw14bu!?d+!HM~#2l^G45x8&UtpR;Os4GxRv>|wm zqHI>C*4v_2^7s!FK(sd`miO{~!8tUtHI#o<6>`){Qar zTdl*?fw%>NCRstK`PO**+7zzWxM2rW@9OBqcjC$XaXyF{{bC$2KveGLPw$%7>=moV zc}-gj_KAiS3HFqDOpV=EV1LYgKuil95ZC}dy+5u7B@UIw!S>9n+|>r|DU1v8X_u_O zMGMyCjw)WM{HOplL}APG?QX9#^)q5aAgoG{w)?p0;(xLw;%>9ENG*`4xB|G97;sg1 zaU=NdyvU4Q>9%K;e)LiMx<--{MGN|ey$l1v=%BhCmX0%ZIA2-{Dy z1>C!rI64%a=)wN5R=^DX><q2S5=EkWYt&FTL`K@}E9Y+IT%Ys$;V9%F+^hjRIoR}A{_UiEFYa5?H(ZugFx)1rA z^G&GGHj5Qw7%qC@6`1eBfHp|igVfT7EDG=#+$3=5g|qkZ7cExh@vIm<8DV!QuCNx& zpQH{fme@HvkmLqMcI+j$6NzD(}&*4AKK+3-;?}aFmx!^ISXzu zT=0|WZ^vK(M0d2gVfV7XHb1R<-Thau(=i9lox@ZAKbTd-TA&y$KMYj z*dM`|I?>E2cE~2qN|Q6N>x>RVNtWx{vkz2rS{W z*$G0CPx2JSnm?~?7G@j-SlKZ?Q>i~Tb9_n}>??!>;c?(| zD;xV?=jYp+Pt9^Ci%Y{%f2^_%V;jBWwTsnq<&+peBfc|J8Zs#@lj9;D_sR{%+gsUbxa{2($){g|Lxz%)lc>OFA)Z2mx}&zxc`5$A>7XZ literal 0 HcmV?d00001 From 86758a0b9518fbf104f0efc79a86fe6224b4fc9e Mon Sep 17 00:00:00 2001 From: Niko Pitkonen <78962935+pitkni@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:10:58 +0300 Subject: [PATCH 6/8] HAI-1736 Update copyright footer to adhere to OSM licence (#386) --- src/common/components/footer/Footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() { - + ); } From b8900724fa12ea3f398b0b7e6436b7e89b48f717 Mon Sep 17 00:00:00 2001 From: Topias Heinonen Date: Tue, 10 Oct 2023 13:09:12 +0300 Subject: [PATCH 7/8] HAI-1944 Add instructions for translation scripts to README (#388) --- .gitignore | 4 ++++ README.md | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) 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 From bedc0f9b01273727b3801d59686f90e5fe2fe5ba Mon Sep 17 00:00:00 2001 From: Niko Pitkonen <78962935+pitkni@users.noreply.github.com> Date: Tue, 10 Oct 2023 13:26:19 +0300 Subject: [PATCH 8/8] HAI-1866 Add length limit for hanke name (#389) Haitaton backend has a limit for hanke name (100 characters). Include this limit in the frontend. --- src/common/components/textInput/TextInput.tsx | 3 +++ src/domain/hanke/edit/HankeForm.test.tsx | 17 +++++++++++++++++ src/domain/hanke/edit/HankeFormPerustiedot.tsx | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) 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/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')}

- +