Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFR] Introduce <PasswordInput> #3013

Merged
merged 2 commits into from
Dec 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/Inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,32 @@ You can customize the `step` props (which defaults to "any"):

`<NumberInput>` also accepts the [common input props](./Inputs.md#common-input-props).

## `<PasswordInput>`

`<PasswordInput>` works like the [`<TextInput>`](#textinput) but overwrites its `type` prop to `password` or `text` in accordance with a visibility button, hidden by default.

```jsx
import { PasswordInput } from 'react-admin';
<PasswordInput source="password" />
```

![Password Input](./img/password-input.png)

It is possible to change the default behavior and display the value by default via the `initiallyVisible` prop:

```jsx
import { PasswordInput } from 'react-admin';
<PasswordInput source="password" initiallyVisible />
```

![Password Input (visible)](./img/password-input-visible.png)

**Tip**: It is possible to set the [`autocomplete` attribute](https://developer.mozilla.org/fr/docs/Web/HTML/Attributs/autocomplete) by injecting an input props:

```jsx
<PasswordInput source="password" inputProps={{ autocomplete: 'current-password' }} />
```

## `<RadioButtonGroupInput>`

If you want to let the user choose a value among a list of possible values that are always shown (instead of hiding them behind a dropdown list, as in [`<SelectInput>`](#selectinput)), `<RadioButtonGroupInput>` is the right component. Set the `choices` attribute to determine the options (with `id`, `name` tuples):
Expand Down
1 change: 1 addition & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ title: "Reference"
* [`<NumberField>`](./Fields.md#numberfield)
* [`<NumberInput>`](./Inputs.md#numberinput)
* [`<Pagination>`](./List.md#pagination)
* [`<PasswordInput>`](./Inputs.md#passwordinput)
* [`<Query>`](./Actions.md#legacy-components-query-mutation-and-withdataprovider)
* [`<RadioButtonGroupInput>`](./Inputs.md#radiobuttongroupinput)
* [`<ReferenceArrayField>`](./Fields.md#referencearrayfield)
Expand Down
Binary file added docs/img/password-input-visible.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/password-input.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions examples/demo/src/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,24 @@ export default {
last_seen_gte: 'Visited Since',
name: 'Name',
total_spent: 'Total spent',
password: 'Password',
confirm_password: 'Confirm password',
},
fieldGroups: {
identity: 'Identity',
address: 'Address',
stats: 'Stats',
history: 'History',
password: 'Password',
change_password: 'Change Password',
},
page: {
delete: 'Delete Customer',
},
errors: {
password_mismatch:
Kmaschta marked this conversation as resolved.
Show resolved Hide resolved
'The password confirmation is not the same as the password.',
},
},
commands: {
name: 'Order |||| Orders',
Expand Down
8 changes: 8 additions & 0 deletions examples/demo/src/i18n/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,24 @@ export default {
name: 'Nom',
total_spent: 'Dépenses',
zipcode: 'Code postal',
password: 'Mot de passe',
confirm_password: 'Confirmez le mot de passe',
},
fieldGroups: {
identity: 'Identité',
address: 'Adresse',
stats: 'Statistiques',
history: 'Historique',
password: 'Mot de passe',
change_password: 'Changer le mot de passe',
},
page: {
delete: 'Supprimer le client',
},
errors: {
password_mismatch:
'La confirmation du mot de passe est différent du mot de passe.',
},
},
commands: {
name: 'Commande |||| Commandes',
Expand Down
29 changes: 28 additions & 1 deletion examples/demo/src/visitors/VisitorCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SimpleForm,
TextInput,
useTranslate,
PasswordInput,
required,
} from 'react-admin';
import { Typography, Box } from '@material-ui/core';
Expand All @@ -23,15 +24,30 @@ export const styles = {
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
password: { display: 'inline-block' },
confirm_password: { display: 'inline-block', marginLeft: 32 },
};

const useStyles = makeStyles(styles);

export const validatePasswords = ({ password, confirm_password }) => {
const errors = {};

if (password && confirm_password && password !== confirm_password) {
errors.confirm_password = [
'resources.customers.errors.password_mismatch',
];
}

return errors;
};

const VisitorCreate = props => {
const classes = useStyles();

return (
<Create {...props}>
<SimpleForm>
<SimpleForm validate={validatePasswords}>
<SectionTitle label="resources.customers.fieldGroups.identity" />
<TextInput
autoFocus
Expand Down Expand Up @@ -63,6 +79,16 @@ const VisitorCreate = props => {
/>
<TextInput source="zipcode" formClassName={classes.zipcode} />
<TextInput source="city" formClassName={classes.city} />
<Separator />
<SectionTitle label="resources.customers.fieldGroups.password" />
<PasswordInput
source="password"
formClassName={classes.password}
/>
<PasswordInput
source="confirm_password"
formClassName={classes.confirm_password}
/>
</SimpleForm>
</Create>
);
Expand All @@ -72,6 +98,7 @@ const requiredValidate = [required()];

const SectionTitle = ({ label }) => {
const translate = useTranslate();

return (
<Typography variant="h6" gutterBottom>
{translate(label)}
Expand Down
34 changes: 34 additions & 0 deletions examples/demo/src/visitors/VisitorEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Edit,
NullableBooleanInput,
TextInput,
Kmaschta marked this conversation as resolved.
Show resolved Hide resolved
PasswordInput,
Toolbar,
useTranslate,
FormWithRedirect,
Expand All @@ -13,6 +14,7 @@ import { Box, Card, CardContent, Typography } from '@material-ui/core';
import Aside from './Aside';
import FullNameField from './FullNameField';
import SegmentsInput from './SegmentsInput';
import { validatePasswords } from './VisitorCreate';

const VisitorEdit = props => {
return (
Expand All @@ -32,9 +34,11 @@ const VisitorTitle = ({ record }) =>

const VisitorForm = props => {
const translate = useTranslate();

return (
<FormWithRedirect
{...props}
validate={validatePasswords}
render={formProps => (
<Card>
<form>
Expand Down Expand Up @@ -127,6 +131,36 @@ const VisitorForm = props => {
/>
</Box>
</Box>

<Box mt="1em" />

<Typography variant="h6" gutterBottom>
{translate(
'resources.customers.fieldGroups.change_password'
)}
</Typography>
<Box display={{ xs: 'block', sm: 'flex' }}>
<Box
flex={1}
mr={{ xs: 0, sm: '0.5em' }}
>
<PasswordInput
source="password"
resource="customers"
fullWidth
/>
</Box>
<Box
flex={1}
ml={{ xs: 0, sm: '0.5em' }}
>
<PasswordInput
source="confirm_password"
resource="customers"
fullWidth
/>
</Box>
</Box>
</Box>
<Box
flex={1}
Expand Down
4 changes: 4 additions & 0 deletions packages/ra-language-english/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ module.exports = {
single_missing:
'Associated reference no longer appears to be available.',
},
password: {
toggle_visible: 'Hide password',
toggle_hidden: 'Show password',
},
},
message: {
about: 'About',
Expand Down
4 changes: 4 additions & 0 deletions packages/ra-language-french/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ module.exports = {
single_missing:
'La référence associée ne semble plus disponible.',
},
password: {
toggle_visible: 'Cacher le mot de passe',
toggle_hidden: 'Montrer le mot de passe',
},
},
message: {
about: 'Au sujet de',
Expand Down
48 changes: 48 additions & 0 deletions packages/ra-ui-materialui/src/input/PasswordInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { FC, useState } from 'react';
import { useTranslate } from 'ra-core';
import { InputAdornment, IconButton } from '@material-ui/core';
import Visibility from '@material-ui/icons/Visibility';
import VisibilityOff from '@material-ui/icons/VisibilityOff';

import TextInput, { TextInputProps } from './TextInput';

export interface PasswordInputProps extends TextInputProps {
initiallyVisible?: boolean;
}

const PasswordInput: FC<PasswordInputProps> = ({
initiallyVisible = false,
...props
}) => {
const [visible, setVisible] = useState(initiallyVisible);
const translate = useTranslate();

const handleClick = () => {
setVisible(!visible);
};

return (
<TextInput
{...props}
type={visible ? 'text' : 'password'}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label={translate(
visible
? 'ra.input.password.toggle_visible'
: 'ra.input.password.toggle_hidden'
)}
onClick={handleClick}
>
{visible ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
),
}}
/>
);
};

export default PasswordInput;
7 changes: 4 additions & 3 deletions packages/ra-ui-materialui/src/input/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import ResettableTextField from './ResettableTextField';
import InputHelperText from './InputHelperText';
import sanitizeRestProps from './sanitizeRestProps';

export type TextInputProps = InputProps<TextFieldProps> &
Omit<TextFieldProps, 'label' | 'helperText'>;

/**
* An Input component for a string
*
Expand All @@ -21,9 +24,7 @@ import sanitizeRestProps from './sanitizeRestProps';
*
* The object passed as `options` props is passed to the <ResettableTextField> component
*/
export const TextInput: FunctionComponent<
InputProps<TextFieldProps> & Omit<TextFieldProps, 'label' | 'helperText'>
> = ({
export const TextInput: FunctionComponent<TextInputProps> = ({
label,
format,
helperText,
Expand Down
2 changes: 2 additions & 0 deletions packages/ra-ui-materialui/src/input/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import InputPropTypes from './InputPropTypes';
import Labeled from './Labeled';
import NullableBooleanInput from './NullableBooleanInput';
import NumberInput from './NumberInput';
import PasswordInput from './PasswordInput';
import RadioButtonGroupInput from './RadioButtonGroupInput';
import ReferenceArrayInput from './ReferenceArrayInput';
import ReferenceInput from './ReferenceInput';
Expand All @@ -36,6 +37,7 @@ export {
Labeled,
NullableBooleanInput,
NumberInput,
PasswordInput,
RadioButtonGroupInput,
ReferenceArrayInput,
ReferenceInput,
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4981,6 +4981,14 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"

data-generator-retail@^2.7.0:
version "2.9.5"
resolved "https://registry.yarnpkg.com/data-generator-retail/-/data-generator-retail-2.9.5.tgz#9a1a541f7bc19c00b633b0d17e6b39837e398976"
integrity sha512-scx3c91hhTMxFIvNgAO2Y2Xor4eIR8FfZ7LBCHVlsUt7DEIUvH6juHIVjT6ytzQjwBuR03UzEOlZpIMim1+KqQ==
dependencies:
date-fns "~1.29.0"
faker "^4.1.0"

data-urls@^1.0.0, data-urls@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
Expand Down