Skip to content

Commit

Permalink
💄(website) replace grommet input by cunningham input
Browse files Browse the repository at this point in the history
- replace grommet input by cunningham input
- move PrivateTextInputField to root/components/Text
- refacto forms to fit new cunningham input text
  • Loading branch information
AntoLC committed Sep 6, 2023
1 parent ad42303 commit f1bc86e
Show file tree
Hide file tree
Showing 28 changed files with 408 additions and 383 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Versioning](https://semver.org/spec/v2.0.0.html).
- Add playlist is_claimable attribute
- Add the Accept-Language header to most of the http request (#2394)

### Changed

- Replace grommet TextInput by Cunningham Input (#2374)

## [4.3.1] - 2023-08-31

### Fixed
Expand Down
12 changes: 6 additions & 6 deletions src/backend/marsha/e2e/test_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def user_logged_in(context: BrowserContext, live_server: LiveServer):

page = context.new_page()
page.goto(live_server.url)
page.fill("input[name=username]", "jane")
page.fill("input[name=password]", "password")
page.fill("input[name=username]", "jane", force=True)
page.fill("input[name=password]", "password", force=True)
page.click("text=OK")
page.wait_for_timeout(500)
page.wait_for_load_state("networkidle")
Expand All @@ -62,8 +62,8 @@ def test_site_login(page: Page, live_server: LiveServer):
user.set_password("password")

page.goto(live_server.url)
page.fill("input[name=username]", "john")
page.fill("input[name=password]", "password")
page.fill("input[name=username]", "john", force=True)
page.fill("input[name=password]", "password", force=True)
page.click("text=OK")

expect(page.get_by_role("menuitem", name="Dashboard")).to_be_visible()
Expand Down Expand Up @@ -299,8 +299,8 @@ def test_playlist_update(context: BrowserContext, live_server: LiveServer):
page = context.pages[0]
page.get_by_role("menuitem", name="My Playlists").click()
page.get_by_role("button", name="Update playlist Playlist test").click()
page.get_by_label("Name*required").click()
page.get_by_label("Name*required").fill("Playlist test updated")
page.get_by_label("Name").click()
page.get_by_label("Name").fill("Playlist test updated")
page.get_by_role("button", name="Save").click()

expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('<PrivateTextInputField />', () => {
expect(screen.getByLabelText('label')).toBeInTheDocument();

await userEvent.click(
screen.getByRole('button', { name: 'show-content.svg' }),
screen.getByRole('button', { name: 'Show the password.' }),
);

await userEvent.type(
Expand All @@ -28,7 +28,7 @@ describe('<PrivateTextInputField />', () => {
expect(input).toBeInTheDocument();

await userEvent.click(
screen.getByRole('button', { name: 'hide-content.svg' }),
screen.getByRole('button', { name: 'Hide the password.' }),
);

await waitFor(() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Input } from '@openfun/cunningham-react';
import { Button } from 'grommet';
import { ComponentPropsWithRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';

import { ReactComponent as HideContent } from 'assets/svg/hide-content.svg';
import { ReactComponent as ShowContent } from 'assets/svg/show-content.svg';

const messages = defineMessages({
revealPassword: {
defaultMessage: 'Show the password.',
description: 'A18n label for the password button.',
id: 'components.Text.PrivateTextInputfield.revealPassword',
},
hidePassword: {
defaultMessage: 'Hide the password.',
description: 'A18n label for the hidden password button.',
id: 'components.Text.PrivateTextInputfield.hidePassword',
},
});

type InputProps = ComponentPropsWithRef<typeof Input>;

interface PrivateTextInputFieldProps extends InputProps {
revealButtonLabel?: string;
hideButtonLabel?: string;
}

export const PrivateTextInputField = ({
label,
revealButtonLabel,
hideButtonLabel,
...inputProps
}: PrivateTextInputFieldProps) => {
const intl = useIntl();
const [isHidden, setIsHidden] = useState(true);

return (
<Input
aria-label={label}
label={label}
fullWidth
required
rightIcon={
<Button
a11yTitle={
isHidden
? revealButtonLabel || intl.formatMessage(messages.revealPassword)
: hideButtonLabel || intl.formatMessage(messages.hidePassword)
}
plain
style={{ margin: '0 1rem' }}
icon={isHidden ? <ShowContent /> : <HideContent />}
onClick={() => setIsHidden(!isHidden)}
/>
}
type={isHidden ? 'password' : 'text'}
{...inputProps}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { UseMutationOptions, useMutation } from '@tanstack/react-query';
import { TokenResponse, fetchWrapper, useJwt } from 'lib-components';
import {
FetchResponseError,
TokenResponse,
fetchResponseHandler,
fetchWrapper,
useJwt,
} from 'lib-components';

type UseBasicLoginData = {
username: string;
password: string;
};
export type UseBasicLoginError = { code: 'exception'; detail?: string };

export interface UseBasicLoginErrorBody {
username?: string[];
password?: string[];
detail?: string;
}

export type UseBasicLoginError = FetchResponseError<UseBasicLoginErrorBody>;

type UseBasicLoginOptions = UseMutationOptions<
TokenResponse,
Expand All @@ -26,14 +39,9 @@ const actionLogin = async ({
body: JSON.stringify(object),
});

if (!response.ok) {
if (response.status === 400 || response.status === 401) {
throw { code: 'invalid', ...(await response.json()) };
}
throw { code: 'exception' };
}

return (await response.json()) as TokenResponse;
return await fetchResponseHandler(response, {
invalidStatus: [400, 401],
});
};

export const useBasicLogin = (options?: UseBasicLoginOptions) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ describe('<LoginFrom />', () => {
expect(
screen.getByRole('textbox', { name: /username/i }),
).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByLabelText('Password')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /OK/i })).toBeInTheDocument();

const buttonHide = screen.getByRole('button', { name: /Hide/i });
expect(buttonHide).toBeInTheDocument();
const buttonShow = screen.getByRole('button', { name: /Show/i });
expect(buttonShow).toBeInTheDocument();

await userEvent.click(buttonHide);
await userEvent.click(buttonShow);

expect(screen.getByRole('button', { name: /View/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Hide/i })).toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /Hide/i }),
screen.queryByRole('button', { name: /Show/i }),
).not.toBeInTheDocument();
expect(
screen.getByRole('textbox', { name: /password/i }),
Expand All @@ -58,15 +58,11 @@ describe('<LoginFrom />', () => {

await userEvent.click(screen.getByRole('button', { name: /OK/i }));

expect(
screen.getAllByText(/This field is required to login./i),
).toHaveLength(2);

await userEvent.type(
screen.getByRole('textbox', { name: /username/i }),
'my_user',
);
await userEvent.type(screen.getByLabelText(/password/i), 'my_pass');
await userEvent.type(screen.getByLabelText('Password'), 'my_pass');
await userEvent.click(screen.getByRole('button', { name: /OK/i }));

expect(
Expand Down Expand Up @@ -104,7 +100,7 @@ describe('<LoginFrom />', () => {
screen.getByRole('textbox', { name: /username/i }),
'my_user',
);
await userEvent.type(screen.getByLabelText(/password/i), 'my_pass');
await userEvent.type(screen.getByLabelText('Password'), 'my_pass');
await userEvent.click(screen.getByRole('button', { name: /OK/i }));

await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/login'));
Expand Down Expand Up @@ -132,7 +128,7 @@ describe('<LoginFrom />', () => {
screen.getByRole('textbox', { name: /username/i }),
'my_user',
);
await userEvent.type(screen.getByLabelText(/password/i), 'my_pass');
await userEvent.type(screen.getByLabelText('Password'), 'my_pass');
await userEvent.click(screen.getByRole('button', { name: /OK/i }));

await waitFor(() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Box, Button, Form, FormField, Text, TextInput } from 'grommet';
import { Alert, FormView, Hide } from 'grommet-icons';
import { Field, Input } from '@openfun/cunningham-react';
import { Box, Text } from 'grommet';
import { Alert } from 'grommet-icons';
import { ButtonLoader } from 'lib-components';
import { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link, useLocation, useNavigate } from 'react-router-dom';

import { PrivateTextInputField } from 'components/Text/PrivateTextInputField';
import { routes } from 'routes/routes';
import { getLocalStorage } from 'utils/browser';

Expand Down Expand Up @@ -50,11 +52,10 @@ export const LoginForm = () => {
const navigate = useNavigate();
const { pathname, search } = useLocation();
const [value, setValue] = useState({ username: '', password: '' });
const [reveal, setReveal] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState<UseBasicLoginError['body']>();
const { mutate: basicLogin, isLoading } = useBasicLogin({
onError: (error: UseBasicLoginError) => {
setMessage(error.detail || intl.formatMessage(messages.error));
onError: (backError) => {
setError(backError.body);
},
onSuccess: () => {
const targetUri = getLocalStorage()?.getItem(TARGET_URL_STORAGE_KEY);
Expand All @@ -77,46 +78,38 @@ export const LoginForm = () => {
}, [navigate, pathname, search]);

return (
<Form
value={value}
onChange={(updateValue) => setValue(updateValue)}
onSubmit={({ value: submitValue }) => basicLogin(submitValue)}
messages={{
required: intl.formatMessage(messages.requiredField),
<form
onSubmit={(e) => {
e.preventDefault();
basicLogin(value);
}}
onChange={(e) => {
const { name, value } = e.target as HTMLInputElement;
setValue((_value) => ({ ..._value, [name]: value }));
setError(undefined);
}}
>
<FormField
<Input
aria-label={intl.formatMessage(messages.usernameLabel)}
label={intl.formatMessage(messages.usernameLabel)}
name="username"
type="username"
fullWidth
required
>
<TextInput
aria-label={intl.formatMessage(messages.usernameLabel)}
name="username"
type="username"
readOnly
onFocus={(e) => e.currentTarget.removeAttribute('readonly')}
state={error?.username ? 'error' : undefined}
text={error?.username?.join(' ')}
/>
<Field className="mt-s" fullWidth>
<PrivateTextInputField
autoComplete="current-password"
name="password"
label={intl.formatMessage(messages.passwordLabel)}
state={error?.password ? 'error' : undefined}
text={error?.password?.join(' ')}
/>
</FormField>
<FormField
label={intl.formatMessage(messages.passwordLabel)}
name="password"
margin={{ bottom: 'xsmall' }}
required
>
<Box direction="row" fill>
<TextInput
aria-label={intl.formatMessage(messages.passwordLabel)}
name="password"
plain
type={reveal ? 'text' : 'password'}
/>
<Button
plain
style={{ margin: '0 1rem' }}
icon={reveal ? <FormView size="medium" /> : <Hide size="medium" />}
onClick={() => setReveal(!reveal)}
/>
</Box>
</FormField>
</Field>
<Link to={routes.PASSWORD_RESET.path}>
<Text
color="blue-active"
Expand All @@ -127,7 +120,7 @@ export const LoginForm = () => {
{intl.formatMessage(messages.passwordLost)}
</Text>
</Link>
{message && (
{error?.detail && (
<Box
direction="row"
align="center"
Expand All @@ -137,7 +130,7 @@ export const LoginForm = () => {
>
<Alert size="medium" color="#df8c00" />
<Text weight="bold" size="small">
{message}
{error?.detail}
</Text>
</Box>
)}
Expand All @@ -147,6 +140,6 @@ export const LoginForm = () => {
isSubmitting={isLoading}
/>
</Box>
</Form>
</form>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,24 @@ describe('<PasswordResetConfirmForm />', () => {
).toBeInTheDocument();

const [hidePassword, hidePasswordConfirm] = screen.getAllByRole('button', {
name: /Hide/i,
name: /Show/i,
});
expect(hidePassword).toBeInTheDocument();
expect(hidePasswordConfirm).toBeInTheDocument();

await userEvent.click(hidePassword);

expect(screen.getByRole('button', { name: /View/i })).toBeInTheDocument(); // One password displayed
expect(screen.getByRole('button', { name: /Hide/i })).toBeInTheDocument(); // The other password hidden
expect(screen.getByRole('button', { name: /Hide/i })).toBeInTheDocument(); // One password displayed
expect(screen.getByRole('button', { name: /Show/i })).toBeInTheDocument(); // The other password hidden
expect(
screen.getByRole('textbox', { name: /password/i }),
).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', { name: /Hide/i }));
await userEvent.click(screen.getByRole('button', { name: /Show/i }));

expect(screen.getAllByRole('button', { name: /View/i }).length).toEqual(2); // Both passwords displayed
expect(screen.getAllByRole('button', { name: /Hide/i }).length).toEqual(2); // Both passwords displayed
expect(
screen.getByRole('textbox', { name: /password confirm/i }),
screen.getByRole('textbox', { name: /Confirm new password/i }),
).toBeInTheDocument();
});

Expand All @@ -60,8 +60,6 @@ describe('<PasswordResetConfirmForm />', () => {
screen.getByRole('button', { name: /Reset my password/i }),
);

expect(screen.getAllByText(/This field is required./i)).toHaveLength(2);

const [newPassword, newPasswordConfirm] =
screen.getAllByLabelText(/Password/i);

Expand Down
Loading

0 comments on commit f1bc86e

Please sign in to comment.