Skip to content

Commit

Permalink
Merge branch 'dev' into HAI-1814
Browse files Browse the repository at this point in the history
  • Loading branch information
markohaarni committed Oct 10, 2023
2 parents aa5fc00 + bedc0f9 commit 9bc8020
Show file tree
Hide file tree
Showing 24 changed files with 348 additions and 101 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Binary file added public/helsinki.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/common/components/footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function HaitatonFooter() {
<Footer.Item as={Link} to={ACCESSIBILITY.path} label={ACCESSIBILITY.label} />
<Footer.Item as={Link} to={PRIVACY_POLICY.path} label={PRIVACY_POLICY.label} />
</Footer.Navigation>
<Footer.Base copyrightHolder="Helsingin kaupunki" />
<Footer.Base copyrightHolder="Helsingin kaupunki, OpenStreetMap contributors" />
</Footer>
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/common/components/textInput/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getInputErrorText } from '../../utils/form';
type PropTypes = {
name: string;
label?: string;
maxLength?: number | undefined;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
Expand All @@ -23,6 +24,7 @@ type PropTypes = {
const TextInput: React.FC<React.PropsWithChildren<PropTypes>> = ({
name,
label,
maxLength = undefined,
disabled,
tooltip,
required,
Expand All @@ -48,6 +50,7 @@ const TextInput: React.FC<React.PropsWithChildren<PropTypes>> = ({
className={className}
label={label || t(`hankeForm:labels:${name}`)}
value={value || ''}
maxLength={maxLength}
helperText={helperText}
placeholder={placeholder}
errorText={getInputErrorText(t, error)}
Expand Down
33 changes: 29 additions & 4 deletions src/domain/application/applicationView/ApplicationView.test.tsx
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -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(<ApplicationViewContainer id={4} />);

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');
Expand All @@ -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(<ApplicationViewContainer id={4} />);

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' }));

Expand All @@ -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<SignedInUser>({
hankeKayttajaId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
kayttooikeustaso: 'KATSELUOIKEUS',
kayttooikeudet: ['VIEW'],
}),
);
}),
);

render(<ApplicationViewContainer id={4} />);

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) => {
Expand All @@ -103,6 +127,7 @@ test('Should not send multiple requests if clicking application cancel confirm b
const { user } = render(<ApplicationViewContainer id={1} />);

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' });
Expand Down
31 changes: 18 additions & 13 deletions src/domain/application/applicationView/ApplicationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,21 +120,25 @@ function ApplicationView({ application, hanke, onEditApplication }: Props) {

<InformationViewHeaderButtons>
{isPending ? (
<Button
theme="coat"
iconLeft={<IconPen aria-hidden="true" />}
onClick={onEditApplication}
>
{t('hakemus:buttons:editApplication')}
</Button>
<UserRightsCheck requiredRight="EDIT_APPLICATIONS" hankeTunnus={hanke?.hankeTunnus}>
<Button
theme="coat"
iconLeft={<IconPen aria-hidden="true" />}
onClick={onEditApplication}
>
{t('hakemus:buttons:editApplication')}
</Button>
</UserRightsCheck>
) : null}
{hanke ? (
<ApplicationCancel
applicationId={id}
alluStatus={alluStatus}
hankeTunnus={hanke?.hankeTunnus}
buttonIcon={<IconTrash aria-hidden />}
/>
<UserRightsCheck requiredRight="EDIT_APPLICATIONS" hankeTunnus={hanke?.hankeTunnus}>
<ApplicationCancel
applicationId={id}
alluStatus={alluStatus}
hankeTunnus={hanke?.hankeTunnus}
buttonIcon={<IconTrash aria-hidden />}
/>
</UserRightsCheck>
) : null}
</InformationViewHeaderButtons>
</InformationViewHeader>
Expand Down
4 changes: 2 additions & 2 deletions src/domain/hanke/accessRights/AccessRightsViewContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 (
Expand Down
17 changes: 17 additions & 0 deletions src/domain/hanke/edit/HankeForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<HankeFormContainer />);
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(<HankeFormContainer hankeTunnus="HAI22-1" />);

Expand Down
2 changes: 1 addition & 1 deletion src/domain/hanke/edit/HankeFormPerustiedot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const HankeFormPerustiedot: React.FC<React.PropsWithChildren<FormProps>> = ({
<p>{t('form:requiredInstruction')}</p>
</Box>
<div className="formWpr">
<TextInput name={FORMFIELD.NIMI} required />
<TextInput name={FORMFIELD.NIMI} maxLength={100} required />
</div>
<div className="formWpr">
<TextArea
Expand Down
43 changes: 43 additions & 0 deletions src/domain/hanke/hankeUsers/UserRightsCheck.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<UserRightsCheck requiredRight="EDIT" hankeTunnus="HAI22-2">
<p>Children</p>
</UserRightsCheck>,
);

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<SignedInUser>({
hankeKayttajaId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
kayttooikeustaso: 'KATSELUOIKEUS',
kayttooikeudet: ['VIEW'],
}),
);
}),
);

render(
<UserRightsCheck requiredRight="EDIT" hankeTunnus="HAI22-2">
<p>Children</p>
</UserRightsCheck>,
);

await waitFor(() => {
expect(screen.queryByText('Children')).not.toBeInTheDocument();
});
});
29 changes: 29 additions & 0 deletions src/domain/hanke/hankeUsers/UserRightsCheck.tsx
Original file line number Diff line number Diff line change
@@ -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;
24 changes: 13 additions & 11 deletions src/domain/hanke/hankeUsers/hankeUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<keyof typeof Rights>;

export type SignedInUser = {
hankeKayttajaId: string;
Expand Down
4 changes: 2 additions & 2 deletions src/domain/hanke/hankeUsers/hankeUsersApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SignedInUser> {
// Get user id and rights of the signed in user for a hanke
export async function getSignedInUserForHanke(hankeTunnus?: string): Promise<SignedInUser> {
const { data } = await api.get<SignedInUser>(`hankkeet/${hankeTunnus}/whoami`);
return data;
}
Expand Down
9 changes: 0 additions & 9 deletions src/domain/hanke/hankeUsers/hooks/useUserRights.ts

This file was deleted.

16 changes: 16 additions & 0 deletions src/domain/hanke/hankeUsers/hooks/useUserRightsForHanke.ts
Original file line number Diff line number Diff line change
@@ -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>(
['signedInUser', hankeTunnus],
() => getSignedInUserForHanke(hankeTunnus),
{
enabled: Boolean(hankeTunnus) && features.accessRights,
},
);
}
Loading

0 comments on commit 9bc8020

Please sign in to comment.