diff --git a/docs/Inputs.md b/docs/Inputs.md index 5e42f4fb32b..f5ddcf2e9c8 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -586,6 +586,32 @@ You can customize the `step` props (which defaults to "any"): `` also accepts the [common input props](./Inputs.md#common-input-props). +## `` + +`` works like the [``](#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'; + +``` + +![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'; + +``` + +![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 + +``` + ## `` 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)), `` is the right component. Set the `choices` attribute to determine the options (with `id`, `name` tuples): diff --git a/docs/Reference.md b/docs/Reference.md index 13c963fd2e7..bc548bb9bbf 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -64,6 +64,7 @@ title: "Reference" * [``](./Fields.md#numberfield) * [``](./Inputs.md#numberinput) * [``](./List.md#pagination) +* [``](./Inputs.md#passwordinput) * [``](./Actions.md#legacy-components-query-mutation-and-withdataprovider) * [``](./Inputs.md#radiobuttongroupinput) * [``](./Fields.md#referencearrayfield) diff --git a/docs/img/password-input-visible.png b/docs/img/password-input-visible.png new file mode 100644 index 00000000000..6833f836714 Binary files /dev/null and b/docs/img/password-input-visible.png differ diff --git a/docs/img/password-input.png b/docs/img/password-input.png new file mode 100644 index 00000000000..8133f0e324e Binary files /dev/null and b/docs/img/password-input.png differ diff --git a/examples/demo/src/i18n/en.js b/examples/demo/src/i18n/en.js index 0c036f5474e..261809eca29 100644 --- a/examples/demo/src/i18n/en.js +++ b/examples/demo/src/i18n/en.js @@ -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: + 'The password confirmation is not the same as the password.', + }, }, commands: { name: 'Order |||| Orders', diff --git a/examples/demo/src/i18n/fr.js b/examples/demo/src/i18n/fr.js index bb4931cc597..6602d0efa13 100644 --- a/examples/demo/src/i18n/fr.js +++ b/examples/demo/src/i18n/fr.js @@ -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', diff --git a/examples/demo/src/visitors/VisitorCreate.js b/examples/demo/src/visitors/VisitorCreate.js index 2d348c9644e..672910f80b7 100644 --- a/examples/demo/src/visitors/VisitorCreate.js +++ b/examples/demo/src/visitors/VisitorCreate.js @@ -5,6 +5,7 @@ import { SimpleForm, TextInput, useTranslate, + PasswordInput, required, } from 'react-admin'; import { Typography, Box } from '@material-ui/core'; @@ -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 ( - + { /> + + + + ); @@ -72,6 +98,7 @@ const requiredValidate = [required()]; const SectionTitle = ({ label }) => { const translate = useTranslate(); + return ( {translate(label)} diff --git a/examples/demo/src/visitors/VisitorEdit.js b/examples/demo/src/visitors/VisitorEdit.js index 079111c5ff0..bb0d99ecd25 100644 --- a/examples/demo/src/visitors/VisitorEdit.js +++ b/examples/demo/src/visitors/VisitorEdit.js @@ -4,6 +4,7 @@ import { Edit, NullableBooleanInput, TextInput, + PasswordInput, Toolbar, useTranslate, FormWithRedirect, @@ -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 ( @@ -32,9 +34,11 @@ const VisitorTitle = ({ record }) => const VisitorForm = props => { const translate = useTranslate(); + return ( (
@@ -127,6 +131,36 @@ const VisitorForm = props => { /> + + + + + {translate( + 'resources.customers.fieldGroups.change_password' + )} + + + + + + + + + = ({ + initiallyVisible = false, + ...props +}) => { + const [visible, setVisible] = useState(initiallyVisible); + const translate = useTranslate(); + + const handleClick = () => { + setVisible(!visible); + }; + + return ( + + + {visible ? : } + + + ), + }} + /> + ); +}; + +export default PasswordInput; diff --git a/packages/ra-ui-materialui/src/input/TextInput.tsx b/packages/ra-ui-materialui/src/input/TextInput.tsx index f63b17db29e..00ff175e1c0 100644 --- a/packages/ra-ui-materialui/src/input/TextInput.tsx +++ b/packages/ra-ui-materialui/src/input/TextInput.tsx @@ -7,6 +7,9 @@ import ResettableTextField from './ResettableTextField'; import InputHelperText from './InputHelperText'; import sanitizeRestProps from './sanitizeRestProps'; +export type TextInputProps = InputProps & + Omit; + /** * An Input component for a string * @@ -21,9 +24,7 @@ import sanitizeRestProps from './sanitizeRestProps'; * * The object passed as `options` props is passed to the component */ -export const TextInput: FunctionComponent< - InputProps & Omit -> = ({ +export const TextInput: FunctionComponent = ({ label, format, helperText, diff --git a/packages/ra-ui-materialui/src/input/index.ts b/packages/ra-ui-materialui/src/input/index.ts index 0dcdaa4220e..3c9f82be4da 100644 --- a/packages/ra-ui-materialui/src/input/index.ts +++ b/packages/ra-ui-materialui/src/input/index.ts @@ -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'; @@ -36,6 +37,7 @@ export { Labeled, NullableBooleanInput, NumberInput, + PasswordInput, RadioButtonGroupInput, ReferenceArrayInput, ReferenceInput, diff --git a/yarn.lock b/yarn.lock index f5b307e9712..92b6b56e0b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"