diff --git a/docs/Translation.md b/docs/Translation.md index b01049b35df..d6231206df5 100644 --- a/docs/Translation.md +++ b/docs/Translation.md @@ -100,11 +100,9 @@ You can find translation packages for the following languages: In addition, the previous version of react-admin, called admin-on-rest, was translated in the following languages: -- Arabic ( `ع` ): [aymendhaya/aor-language-arabic](https://github.com/aymendhaya/aor-language-arabic) - Chinese (Traditional) (`cht`): [leesei/aor-language-chinese-traditional](https://github.com/leesei/aor-language-chinese-traditional) - Croatian (`hr`): [ariskemper/aor-language-croatian](https://github.com/ariskemper/aor-language-croatian) - Greek (`el`): [zifnab87/aor-language-greek](https://github.com/zifnab87/aor-language-greek) -- Hebrew (`he`): [motro/aor-language-hebrew](https://github.com/motro/aor-language-hebrew) - Japanese (`ja`): [kuma-guy/aor-language-japanese](https://github.com/kuma-guy/aor-language-japanese) - Slovenian (`sl`): [ariskemper/aor-language-slovenian](https://github.com/ariskemper/aor-language-slovenian) - Swedish (`sv`): [StefanWallin/aor-language-swedish](https://github.com/StefanWallin/aor-language-swedish) @@ -139,31 +137,28 @@ const App = () => ( export default App; ``` -Then, dispatch the `CHANGE_LOCALE` action, by using the `changeLocale` action creator. For instance, the following component switches language between English and French: +Then, dispatch the `CHANGE_LOCALE` action, by using the `changeLocale` action creator. For instance, the following component allows the user to switch the interface language between English and French: ```jsx import React, { Component } from 'react'; -import { connect } from 'react-redux'; +import { useDispatch } from 'react-redux'; import Button from '@material-ui/core/Button'; -import { changeLocale as changeLocaleAction } from 'react-admin'; - -class LocaleSwitcher extends Component { - switchToFrench = () => this.props.changeLocale('fr'); - switchToEnglish = () => this.props.changeLocale('en'); - - render() { - const { changeLocale } = this.props; - return ( -
-
Language
- - -
- ); - } +import { changeLocale } from 'react-admin'; + +const LocaleSwitcher = () => { + const dispatch = useDispatch(); + const switchToFrench = () => dispatch(changeLocale('fr')); + const switchToEnglish = () => dispatch(changeLocale('en')); + return ( +
+
Language
+ + +
+ ); } -export default connect(undefined, { changeLocale: changeLocaleAction })(LocaleSwitcher); +export default LocaleSwitcher; ``` ## Using The Browser Locale @@ -280,25 +275,28 @@ const App = () => ( ); ``` -## Translating Your Own Components +## `useTranslate` Hook -React-admin package provides a `translate` Higher-Order Component, which simply passes the `translate` function as a prop to the wrapped component: +If you need to translate messages in your own components, React-admin provides a `useTranslate` hook, which returns the `translate` function: ```jsx // in src/MyHelloButton.js import React from 'react'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; -const MyHelloButton = ({ translate }) => ( - -); +const MyHelloButton = () => { + const translate = useTranslate(); + return ( + + ); +} -export default translate(MyHelloButton); +export default MyHelloButton; ``` **Tip**: For your message identifiers, choose a different root name than `ra` and `resources`, which are reserved. -**Tip**: Don't use `translate` for Field and Input labels, or for page titles, as they are already translated: +**Tip**: Don't use `useTranslate` for Field and Input labels, or for page titles, as they are already translated: ```jsx // don't do this @@ -312,6 +310,27 @@ export default translate(MyHelloButton); // and translate the `resources.customers.fields.first_name` key ``` +## `withTranslate` HOC + +If you're stuck with class components, react-admin also exports a `withTranslate` higher-order component, which injects the `translate` function as prop. + +```jsx +// in src/MyHelloButton.js +import React, { Component } from 'react'; +import { withTranslate } from 'react-admin'; + +class MyHelloButton extends Component { + render() { + const { translate } = this.props; + return ( + + ); + } +} + +export default withTranslate(MyHelloButton); +``` + ## Using Specific Polyglot Features Polyglot.js is a fantastic library: in addition to being small, fully maintained, and totally framework agnostic, it provides some nice features such as interpolation and pluralization, that you can use in react-admin. diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index df26c6fb300..c13e79d32f3 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -789,7 +789,10 @@ Translations
  • - Translating Your Own Components + useTranslate Hook +
  • +
  • + withTranslate HOC
  • Using Specific Polyglot Features diff --git a/examples/demo/src/categories/CategoryEdit.js b/examples/demo/src/categories/CategoryEdit.js index 12886ebde1e..09f66d1e8cd 100644 --- a/examples/demo/src/categories/CategoryEdit.js +++ b/examples/demo/src/categories/CategoryEdit.js @@ -1,6 +1,5 @@ import React from 'react'; import { - translate, Datagrid, Edit, EditButton, @@ -8,17 +7,21 @@ import { ReferenceManyField, SimpleForm, TextInput, + useTranslate, } from 'react-admin'; import ThumbnailField from '../products/ThumbnailField'; import ProductRefField from '../products/ProductRefField'; -const CategoryTitle = translate(({ record, translate }) => ( - - {translate('resources.categories.name', { smart_count: 1 })} " - {record.name}" - -)); +const CategoryTitle = ({ record }) => { + const translate = useTranslate(); + return ( + + {translate('resources.categories.name', { smart_count: 1 })} " + {record.name}" + + ); +}; const CategoryEdit = props => ( } {...props}> diff --git a/examples/demo/src/categories/LinkToRelatedProducts.js b/examples/demo/src/categories/LinkToRelatedProducts.js index edcc0cfd59f..a7c27f338cb 100644 --- a/examples/demo/src/categories/LinkToRelatedProducts.js +++ b/examples/demo/src/categories/LinkToRelatedProducts.js @@ -1,9 +1,8 @@ import React from 'react'; -import compose from 'recompose/compose'; import Button from '@material-ui/core/Button'; import { withStyles } from '@material-ui/core/styles'; import { Link } from 'react-router-dom'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; import { stringify } from 'query-string'; import products from '../products'; @@ -16,30 +15,29 @@ const styles = { }, }; -const LinkToRelatedProducts = ({ classes, record, translate }) => ( - -); +const LinkToRelatedProducts = ({ classes, record }) => { + const translate = useTranslate(); + return ( + + ); +}; -const enhance = compose( - withStyles(styles), - translate -); -export default enhance(LinkToRelatedProducts); +export default withStyles(styles)(LinkToRelatedProducts); diff --git a/examples/demo/src/configuration/Configuration.js b/examples/demo/src/configuration/Configuration.js index f68cb1771f5..5ef0676ae50 100644 --- a/examples/demo/src/configuration/Configuration.js +++ b/examples/demo/src/configuration/Configuration.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import Button from '@material-ui/core/Button'; -import { translate, changeLocale, Title } from 'react-admin'; +import { useTranslate, changeLocale, Title } from 'react-admin'; import withStyles from '@material-ui/core/styles/withStyles'; import compose from 'recompose/compose'; import { changeTheme } from './actions'; @@ -19,50 +19,54 @@ const Configuration = ({ locale, changeTheme, changeLocale, - translate, -}) => ( - - - <CardContent> - <div className={classes.label}>{translate('pos.theme.name')}</div> - <Button - variant="raised" - className={classes.button} - color={theme === 'light' ? 'primary' : 'default'} - onClick={() => changeTheme('light')} - > - {translate('pos.theme.light')} - </Button> - <Button - variant="raised" - className={classes.button} - color={theme === 'dark' ? 'primary' : 'default'} - onClick={() => changeTheme('dark')} - > - {translate('pos.theme.dark')} - </Button> - </CardContent> - <CardContent> - <div className={classes.label}>{translate('pos.language')}</div> - <Button - variant="raised" - className={classes.button} - color={locale === 'en' ? 'primary' : 'default'} - onClick={() => changeLocale('en')} - > - en - </Button> - <Button - variant="raised" - className={classes.button} - color={locale === 'fr' ? 'primary' : 'default'} - onClick={() => changeLocale('fr')} - > - fr - </Button> - </CardContent> - </Card> -); +}) => { + const translate = useTranslate(); + return ( + <Card> + <Title title={translate('pos.configuration')} /> + <CardContent> + <div className={classes.label}> + {translate('pos.theme.name')} + </div> + <Button + variant="raised" + className={classes.button} + color={theme === 'light' ? 'primary' : 'default'} + onClick={() => changeTheme('light')} + > + {translate('pos.theme.light')} + </Button> + <Button + variant="raised" + className={classes.button} + color={theme === 'dark' ? 'primary' : 'default'} + onClick={() => changeTheme('dark')} + > + {translate('pos.theme.dark')} + </Button> + </CardContent> + <CardContent> + <div className={classes.label}>{translate('pos.language')}</div> + <Button + variant="raised" + className={classes.button} + color={locale === 'en' ? 'primary' : 'default'} + onClick={() => changeLocale('en')} + > + en + </Button> + <Button + variant="raised" + className={classes.button} + color={locale === 'fr' ? 'primary' : 'default'} + onClick={() => changeLocale('fr')} + > + fr + </Button> + </CardContent> + </Card> + ); +}; const mapStateToProps = state => ({ theme: state.theme, @@ -77,7 +81,6 @@ const enhance = compose( changeTheme, } ), - translate, withStyles(styles) ); diff --git a/examples/demo/src/dashboard/MonthlyRevenue.js b/examples/demo/src/dashboard/MonthlyRevenue.js index c05bce16d00..d7faa705ca6 100644 --- a/examples/demo/src/dashboard/MonthlyRevenue.js +++ b/examples/demo/src/dashboard/MonthlyRevenue.js @@ -1,10 +1,9 @@ import React from 'react'; -import compose from 'recompose/compose'; import Card from '@material-ui/core/Card'; import DollarIcon from '@material-ui/icons/AttachMoney'; import { withStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; import CardIcon from './CardIcon'; @@ -22,23 +21,21 @@ const styles = { }, }; -const MonthlyRevenue = ({ value, translate, classes }) => ( - <div className={classes.main}> - <CardIcon Icon={DollarIcon} bgColor="#31708f" /> - <Card className={classes.card}> - <Typography className={classes.title} color="textSecondary"> - {translate('pos.dashboard.monthly_revenue')} - </Typography> - <Typography variant="headline" component="h2"> - {value} - </Typography> - </Card> - </div> -); - -const enhance = compose( - withStyles(styles), - translate -); +const MonthlyRevenue = ({ value, classes }) => { + const translate = useTranslate(); + return ( + <div className={classes.main}> + <CardIcon Icon={DollarIcon} bgColor="#31708f" /> + <Card className={classes.card}> + <Typography className={classes.title} color="textSecondary"> + {translate('pos.dashboard.monthly_revenue')} + </Typography> + <Typography variant="headline" component="h2"> + {value} + </Typography> + </Card> + </div> + ); +}; -export default enhance(MonthlyRevenue); +export default withStyles(styles)(MonthlyRevenue); diff --git a/examples/demo/src/dashboard/NbNewOrders.js b/examples/demo/src/dashboard/NbNewOrders.js index d8f34cce855..fa3719c33c0 100644 --- a/examples/demo/src/dashboard/NbNewOrders.js +++ b/examples/demo/src/dashboard/NbNewOrders.js @@ -1,10 +1,9 @@ import React from 'react'; -import compose from 'recompose/compose'; import Card from '@material-ui/core/Card'; import ShoppingCartIcon from '@material-ui/icons/ShoppingCart'; import { withStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; import CardIcon from './CardIcon'; @@ -22,23 +21,21 @@ const styles = { }, }; -const NbNewOrders = ({ value, translate, classes }) => ( - <div className={classes.main}> - <CardIcon Icon={ShoppingCartIcon} bgColor="#ff9800" /> - <Card className={classes.card}> - <Typography className={classes.title} color="textSecondary"> - {translate('pos.dashboard.new_orders')} - </Typography> - <Typography variant="headline" component="h2"> - {value} - </Typography> - </Card> - </div> -); - -const enhance = compose( - withStyles(styles), - translate -); +const NbNewOrders = ({ value, classes }) => { + const translate = useTranslate(); + return ( + <div className={classes.main}> + <CardIcon Icon={ShoppingCartIcon} bgColor="#ff9800" /> + <Card className={classes.card}> + <Typography className={classes.title} color="textSecondary"> + {translate('pos.dashboard.new_orders')} + </Typography> + <Typography variant="headline" component="h2"> + {value} + </Typography> + </Card> + </div> + ); +}; -export default enhance(NbNewOrders); +export default withStyles(styles)(NbNewOrders); diff --git a/examples/demo/src/dashboard/NewCustomers.js b/examples/demo/src/dashboard/NewCustomers.js index 8474fe7b90e..bb803f16171 100644 --- a/examples/demo/src/dashboard/NewCustomers.js +++ b/examples/demo/src/dashboard/NewCustomers.js @@ -1,5 +1,4 @@ import React, { useMemo } from 'react'; -import compose from 'recompose/compose'; import Card from '@material-ui/core/Card'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; @@ -10,7 +9,7 @@ import Typography from '@material-ui/core/Typography'; import CustomerIcon from '@material-ui/icons/PersonAdd'; import Divider from '@material-ui/core/Divider'; import { Link } from 'react-router-dom'; -import { translate, useQuery, GET_LIST } from 'react-admin'; +import { useTranslate, useQuery, GET_LIST } from 'react-admin'; import CardIcon from './CardIcon'; @@ -40,7 +39,8 @@ const styles = theme => ({ }, }); -const NewCustomers = ({ translate, classes }) => { +const NewCustomers = ({ classes }) => { + const translate = useTranslate(); const aMonthAgo = useMemo(() => { const date = new Date(); date.setDate(date.getDate() - 30); @@ -57,6 +57,7 @@ const NewCustomers = ({ translate, classes }) => { }); if (!loaded) return null; + const nb = visitors.reduce(nb => ++nb, 0); return ( <div className={classes.main}> @@ -99,9 +100,4 @@ const NewCustomers = ({ translate, classes }) => { ); }; -const enhance = compose( - withStyles(styles), - translate -); - -export default enhance(NewCustomers); +export default withStyles(styles)(NewCustomers); diff --git a/examples/demo/src/dashboard/PendingOrders.js b/examples/demo/src/dashboard/PendingOrders.js index 3dfe12c5f65..27fa5354d7f 100644 --- a/examples/demo/src/dashboard/PendingOrders.js +++ b/examples/demo/src/dashboard/PendingOrders.js @@ -1,5 +1,4 @@ import React from 'react'; -import compose from 'recompose/compose'; import Card from '@material-ui/core/Card'; import CardHeader from '@material-ui/core/CardHeader'; import List from '@material-ui/core/List'; @@ -9,7 +8,7 @@ import ListItemText from '@material-ui/core/ListItemText'; import Avatar from '@material-ui/core/Avatar'; import { withStyles } from '@material-ui/core/styles'; import { Link } from 'react-router-dom'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; const style = theme => ({ root: { @@ -24,51 +23,57 @@ const style = theme => ({ }, }); -const PendingOrders = ({ orders = [], customers = {}, translate, classes }) => ( - <Card className={classes.root}> - <CardHeader title={translate('pos.dashboard.pending_orders')} /> - <List dense={true}> - {orders.map(record => ( - <ListItem - key={record.id} - button - component={Link} - to={`/commands/${record.id}`} - > - {customers[record.customer_id] ? ( - <Avatar - className={classes.avatar} - src={`${ - customers[record.customer_id].avatar - }?size=32x32`} +const PendingOrders = ({ orders = [], customers = {}, classes }) => { + const translate = useTranslate(); + return ( + <Card className={classes.root}> + <CardHeader title={translate('pos.dashboard.pending_orders')} /> + <List dense={true}> + {orders.map(record => ( + <ListItem + key={record.id} + button + component={Link} + to={`/commands/${record.id}`} + > + {customers[record.customer_id] ? ( + <Avatar + className={classes.avatar} + src={`${ + customers[record.customer_id].avatar + }?size=32x32`} + /> + ) : ( + <Avatar /> + )} + <ListItemText + primary={new Date(record.date).toLocaleString( + 'en-GB' + )} + secondary={translate('pos.dashboard.order.items', { + smart_count: record.basket.length, + nb_items: record.basket.length, + customer_name: customers[record.customer_id] + ? `${ + customers[record.customer_id] + .first_name + } ${ + customers[record.customer_id] + .last_name + }` + : '', + })} /> - ) : ( - <Avatar /> - )} - <ListItemText - primary={new Date(record.date).toLocaleString('en-GB')} - secondary={translate('pos.dashboard.order.items', { - smart_count: record.basket.length, - nb_items: record.basket.length, - customer_name: customers[record.customer_id] - ? `${ - customers[record.customer_id].first_name - } ${customers[record.customer_id].last_name}` - : '', - })} - /> - <ListItemSecondaryAction> - <span className={classes.cost}>{record.total}$</span> - </ListItemSecondaryAction> - </ListItem> - ))} - </List> - </Card> -); + <ListItemSecondaryAction> + <span className={classes.cost}> + {record.total}$ + </span> + </ListItemSecondaryAction> + </ListItem> + ))} + </List> + </Card> + ); +}; -const enhance = compose( - withStyles(style), - translate -); - -export default enhance(PendingOrders); +export default withStyles(style)(PendingOrders); diff --git a/examples/demo/src/dashboard/PendingReviews.js b/examples/demo/src/dashboard/PendingReviews.js index bb7f3d06a53..5bea04b90ad 100644 --- a/examples/demo/src/dashboard/PendingReviews.js +++ b/examples/demo/src/dashboard/PendingReviews.js @@ -1,5 +1,4 @@ import React from 'react'; -import compose from 'recompose/compose'; import Card from '@material-ui/core/Card'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; @@ -10,7 +9,7 @@ import Typography from '@material-ui/core/Typography'; import CommentIcon from '@material-ui/icons/Comment'; import Divider from '@material-ui/core/Divider'; import { Link } from 'react-router-dom'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; import CardIcon from './CardIcon'; @@ -52,64 +51,56 @@ const location = { query: { filter: JSON.stringify({ status: 'pending' }) }, }; -const PendingReviews = ({ - reviews = [], - customers = {}, - nb, - translate, - classes, -}) => ( - <div className={classes.main}> - <CardIcon Icon={CommentIcon} bgColor="#f44336" /> - <Card className={classes.card}> - <Typography className={classes.title} color="textSecondary"> - {translate('pos.dashboard.pending_reviews')} - </Typography> - <Typography - variant="headline" - component="h2" - className={classes.value} - > - <Link to={location} className={classes.titleLink}> - {nb} - </Link> - </Typography> - <Divider /> - <List> - {reviews.map(record => ( - <ListItem - key={record.id} - button - component={Link} - to={`/reviews/${record.id}`} - > - {customers[record.customer_id] ? ( - <Avatar - src={`${ - customers[record.customer_id].avatar - }?size=32x32`} - className={classes.avatar} - /> - ) : ( - <Avatar /> - )} - - <ListItemText - primary={<StarRatingField record={record} />} - secondary={record.comment} - className={classes.listItemText} - style={{ paddingRight: 0 }} - /> - </ListItem> - ))} - </List> - </Card> - </div> -); +const PendingReviews = ({ reviews = [], customers = {}, nb, classes }) => { + const translate = useTranslate(); + return ( + <div className={classes.main}> + <CardIcon Icon={CommentIcon} bgColor="#f44336" /> + <Card className={classes.card}> + <Typography className={classes.title} color="textSecondary"> + {translate('pos.dashboard.pending_reviews')} + </Typography> + <Typography + variant="headline" + component="h2" + className={classes.value} + > + <Link to={location} className={classes.titleLink}> + {nb} + </Link> + </Typography> + <Divider /> + <List> + {reviews.map(record => ( + <ListItem + key={record.id} + button + component={Link} + to={`/reviews/${record.id}`} + > + {customers[record.customer_id] ? ( + <Avatar + src={`${ + customers[record.customer_id].avatar + }?size=32x32`} + className={classes.avatar} + /> + ) : ( + <Avatar /> + )} -const enhance = compose( - withStyles(styles), - translate -); + <ListItemText + primary={<StarRatingField record={record} />} + secondary={record.comment} + className={classes.listItemText} + style={{ paddingRight: 0 }} + /> + </ListItem> + ))} + </List> + </Card> + </div> + ); +}; -export default enhance(PendingReviews); +export default withStyles(styles)(PendingReviews); diff --git a/examples/demo/src/dashboard/Welcome.js b/examples/demo/src/dashboard/Welcome.js index fec1f8f3b16..2714dfc4456 100644 --- a/examples/demo/src/dashboard/Welcome.js +++ b/examples/demo/src/dashboard/Welcome.js @@ -1,5 +1,4 @@ import React from 'react'; -import compose from 'recompose/compose'; import Card from '@material-ui/core/Card'; import CardActions from '@material-ui/core/CardActions'; import CardContent from '@material-ui/core/CardContent'; @@ -9,7 +8,7 @@ import Typography from '@material-ui/core/Typography'; import HomeIcon from '@material-ui/icons/Home'; import CodeIcon from '@material-ui/icons/Code'; import { withStyles } from '@material-ui/core/styles'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; const styles = { media: { @@ -22,33 +21,31 @@ const mediaUrl = `https://marmelab.com/posters/beard-${parseInt( 10 ) + 1}.jpeg`; -const Welcome = ({ classes, translate }) => ( - <Card> - <CardMedia image={mediaUrl} className={classes.media} /> - <CardContent> - <Typography variant="headline" component="h2"> - {translate('pos.dashboard.welcome.title')} - </Typography> - <Typography component="p"> - {translate('pos.dashboard.welcome.subtitle')} - </Typography> - </CardContent> - <CardActions style={{ justifyContent: 'flex-end' }}> - <Button href="https://marmelab.com/react-admin"> - <HomeIcon style={{ paddingRight: '0.5em' }} /> - {translate('pos.dashboard.welcome.aor_button')} - </Button> - <Button href="https://github.com/marmelab/react-admin/tree/master/examples/demo"> - <CodeIcon style={{ paddingRight: '0.5em' }} /> - {translate('pos.dashboard.welcome.demo_button')} - </Button> - </CardActions> - </Card> -); - -const enhance = compose( - withStyles(styles), - translate -); +const Welcome = ({ classes }) => { + const translate = useTranslate(); + return ( + <Card> + <CardMedia image={mediaUrl} className={classes.media} /> + <CardContent> + <Typography variant="headline" component="h2"> + {translate('pos.dashboard.welcome.title')} + </Typography> + <Typography component="p"> + {translate('pos.dashboard.welcome.subtitle')} + </Typography> + </CardContent> + <CardActions style={{ justifyContent: 'flex-end' }}> + <Button href="https://marmelab.com/react-admin"> + <HomeIcon style={{ paddingRight: '0.5em' }} /> + {translate('pos.dashboard.welcome.aor_button')} + </Button> + <Button href="https://github.com/marmelab/react-admin/tree/master/examples/demo"> + <CodeIcon style={{ paddingRight: '0.5em' }} /> + {translate('pos.dashboard.welcome.demo_button')} + </Button> + </CardActions> + </Card> + ); +}; -export default enhance(Welcome); +export default withStyles(styles)(Welcome); diff --git a/examples/demo/src/layout/AppBar.js b/examples/demo/src/layout/AppBar.js index 4506f2b95ed..35e146c261d 100644 --- a/examples/demo/src/layout/AppBar.js +++ b/examples/demo/src/layout/AppBar.js @@ -1,5 +1,5 @@ import React from 'react'; -import { AppBar, UserMenu, MenuItemLink, translate } from 'react-admin'; +import { AppBar, UserMenu, MenuItemLink, useTranslate } from 'react-admin'; import Typography from '@material-ui/core/Typography'; import SettingsIcon from '@material-ui/icons/Settings'; import { withStyles } from '@material-ui/core/styles'; @@ -18,15 +18,18 @@ const styles = { }, }; -const CustomUserMenu = translate(({ translate, ...props }) => ( - <UserMenu {...props}> - <MenuItemLink - to="/configuration" - primaryText={translate('pos.configuration')} - leftIcon={<SettingsIcon />} - /> - </UserMenu> -)); +const CustomUserMenu = props => { + const translate = useTranslate(); + return ( + <UserMenu {...props}> + <MenuItemLink + to="/configuration" + primaryText={translate('pos.configuration')} + leftIcon={<SettingsIcon />} + /> + </UserMenu> + ); +}; const CustomAppBar = ({ classes, ...props }) => ( <AppBar {...props} userMenu={<CustomUserMenu />}> diff --git a/examples/demo/src/layout/Login.js b/examples/demo/src/layout/Login.js index 92fb967c688..7897fb3d3ab 100644 --- a/examples/demo/src/layout/Login.js +++ b/examples/demo/src/layout/Login.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { propTypes, reduxForm, Field } from 'redux-form'; import { connect } from 'react-redux'; @@ -17,7 +17,7 @@ import { } from '@material-ui/core/styles'; import LockIcon from '@material-ui/icons/Lock'; -import { Notification, translate, userLogin } from 'react-admin'; +import { Notification, useTranslate, translate, userLogin } from 'react-admin'; import { lightTheme } from './themes'; @@ -76,76 +76,68 @@ const renderInput = ({ /> ); -class Login extends Component { - login = auth => - this.props.userLogin( - auth, - this.props.location.state - ? this.props.location.state.nextPathname - : '/' - ); +const Login = ({ classes, handleSubmit, isLoading, location, userLogin }) => { + const translate = useTranslate(); + const login = auth => + userLogin(auth, location.state ? location.state.nextPathname : '/'); - render() { - const { classes, handleSubmit, isLoading, translate } = this.props; - return ( - <div className={classes.main}> - <Card className={classes.card}> - <div className={classes.avatar}> - <Avatar className={classes.icon}> - <LockIcon /> - </Avatar> - </div> - <form onSubmit={handleSubmit(this.login)}> - <div className={classes.hint}>Hint: demo / demo</div> - <div className={classes.form}> - <div className={classes.input}> - <Field - autoFocus - name="username" - component={renderInput} - label={translate('ra.auth.username')} - disabled={isLoading} - /> - </div> - <div className={classes.input}> - <Field - name="password" - component={renderInput} - label={translate('ra.auth.password')} - type="password" - disabled={isLoading} - /> - </div> + return ( + <div className={classes.main}> + <Card className={classes.card}> + <div className={classes.avatar}> + <Avatar className={classes.icon}> + <LockIcon /> + </Avatar> + </div> + <form onSubmit={handleSubmit(login)}> + <div className={classes.hint}>Hint: demo / demo</div> + <div className={classes.form}> + <div className={classes.input}> + <Field + autoFocus + name="username" + component={renderInput} + label={translate('ra.auth.username')} + disabled={isLoading} + /> </div> - <CardActions className={classes.actions}> - <Button - variant="raised" - type="submit" - color="primary" + <div className={classes.input}> + <Field + name="password" + component={renderInput} + label={translate('ra.auth.password')} + type="password" disabled={isLoading} - className={classes.button} - fullWidth - > - {isLoading && ( - <CircularProgress size={25} thickness={2} /> - )} - {translate('ra.auth.sign_in')} - </Button> - </CardActions> - </form> - </Card> - <Notification /> - </div> - ); - } -} + /> + </div> + </div> + <CardActions className={classes.actions}> + <Button + variant="raised" + type="submit" + color="primary" + disabled={isLoading} + className={classes.button} + fullWidth + > + {isLoading && ( + <CircularProgress size={25} thickness={2} /> + )} + {translate('ra.auth.sign_in')} + </Button> + </CardActions> + </form> + </Card> + <Notification /> + </div> + ); +}; Login.propTypes = { ...propTypes, authProvider: PropTypes.func, classes: PropTypes.object, previousRoute: PropTypes.string, - translate: PropTypes.func.isRequired, userLogin: PropTypes.func.isRequired, }; diff --git a/examples/demo/src/layout/Menu.js b/examples/demo/src/layout/Menu.js index 9d11c03b866..a9205818288 100644 --- a/examples/demo/src/layout/Menu.js +++ b/examples/demo/src/layout/Menu.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import compose from 'recompose/compose'; @@ -6,7 +6,7 @@ import SettingsIcon from '@material-ui/icons/Settings'; import LabelIcon from '@material-ui/icons/Label'; import { withRouter } from 'react-router-dom'; import { - translate, + useTranslate, DashboardMenuItem, MenuItemLink, Responsive, @@ -20,127 +20,125 @@ import categories from '../categories'; import reviews from '../reviews'; import SubMenu from './SubMenu'; -class Menu extends Component { - state = { +const Menu = ({ onMenuClick, open, logout }) => { + const [state, setState] = useState({ menuCatalog: false, menuSales: false, menuCustomers: false, - }; - - static propTypes = { - onMenuClick: PropTypes.func, - logout: PropTypes.object, - }; + }); + const translate = useTranslate(); - handleToggle = menu => { - this.setState(state => ({ [menu]: !state[menu] })); + const handleToggle = menu => { + setState(state => ({ ...state, [menu]: !state[menu] })); }; - render() { - const { onMenuClick, open, logout, translate } = this.props; - return ( - <div> - {' '} - <DashboardMenuItem onClick={onMenuClick} /> - <SubMenu - handleToggle={() => this.handleToggle('menuSales')} - isOpen={this.state.menuSales} - sidebarIsOpen={open} - name="pos.menu.sales" - icon={<orders.icon />} - > - <MenuItemLink - to={`/commands`} - primaryText={translate(`resources.commands.name`, { - smart_count: 2, - })} - leftIcon={<orders.icon />} - onClick={onMenuClick} - /> - <MenuItemLink - to={`/invoices`} - primaryText={translate(`resources.invoices.name`, { - smart_count: 2, - })} - leftIcon={<invoices.icon />} - onClick={onMenuClick} - /> - </SubMenu> - <SubMenu - handleToggle={() => this.handleToggle('menuCatalog')} - isOpen={this.state.menuCatalog} - sidebarIsOpen={open} - name="pos.menu.catalog" - icon={<products.icon />} - > - <MenuItemLink - to={`/products`} - primaryText={translate(`resources.products.name`, { - smart_count: 2, - })} - leftIcon={<products.icon />} - onClick={onMenuClick} - /> - <MenuItemLink - to={`/categories`} - primaryText={translate(`resources.categories.name`, { - smart_count: 2, - })} - leftIcon={<categories.icon />} - onClick={onMenuClick} - /> - </SubMenu> - <SubMenu - handleToggle={() => this.handleToggle('menuCustomer')} - isOpen={this.state.menuCustomer} - sidebarIsOpen={open} - name="pos.menu.customers" - icon={<visitors.icon />} - > - <MenuItemLink - to={`/customers`} - primaryText={translate(`resources.customers.name`, { - smart_count: 2, - })} - leftIcon={<visitors.icon />} - onClick={onMenuClick} - /> - <MenuItemLink - to={`/segments`} - primaryText={translate(`resources.segments.name`, { - smart_count: 2, - })} - leftIcon={<LabelIcon />} - onClick={onMenuClick} - /> - </SubMenu> + return ( + <div> + {' '} + <DashboardMenuItem onClick={onMenuClick} /> + <SubMenu + handleToggle={() => handleToggle('menuSales')} + isOpen={state.menuSales} + sidebarIsOpen={open} + name="pos.menu.sales" + icon={<orders.icon />} + > + <MenuItemLink + to={`/commands`} + primaryText={translate(`resources.commands.name`, { + smart_count: 2, + })} + leftIcon={<orders.icon />} + onClick={onMenuClick} + /> + <MenuItemLink + to={`/invoices`} + primaryText={translate(`resources.invoices.name`, { + smart_count: 2, + })} + leftIcon={<invoices.icon />} + onClick={onMenuClick} + /> + </SubMenu> + <SubMenu + handleToggle={() => handleToggle('menuCatalog')} + isOpen={state.menuCatalog} + sidebarIsOpen={open} + name="pos.menu.catalog" + icon={<products.icon />} + > + <MenuItemLink + to={`/products`} + primaryText={translate(`resources.products.name`, { + smart_count: 2, + })} + leftIcon={<products.icon />} + onClick={onMenuClick} + /> <MenuItemLink - to={`/reviews`} - primaryText={translate(`resources.reviews.name`, { + to={`/categories`} + primaryText={translate(`resources.categories.name`, { smart_count: 2, })} - leftIcon={<reviews.icon />} + leftIcon={<categories.icon />} onClick={onMenuClick} /> - <Responsive - xsmall={ - <MenuItemLink - to="/configuration" - primaryText={translate('pos.configuration')} - leftIcon={<SettingsIcon />} - onClick={onMenuClick} - /> - } - medium={null} + </SubMenu> + <SubMenu + handleToggle={() => handleToggle('menuCustomer')} + isOpen={state.menuCustomer} + sidebarIsOpen={open} + name="pos.menu.customers" + icon={<visitors.icon />} + > + <MenuItemLink + to={`/customers`} + primaryText={translate(`resources.customers.name`, { + smart_count: 2, + })} + leftIcon={<visitors.icon />} + onClick={onMenuClick} /> - <Responsive - small={logout} - medium={null} // Pass null to render nothing on larger devices + <MenuItemLink + to={`/segments`} + primaryText={translate(`resources.segments.name`, { + smart_count: 2, + })} + leftIcon={<LabelIcon />} + onClick={onMenuClick} /> - </div> - ); - } -} + </SubMenu> + <MenuItemLink + to={`/reviews`} + primaryText={translate(`resources.reviews.name`, { + smart_count: 2, + })} + leftIcon={<reviews.icon />} + onClick={onMenuClick} + /> + <Responsive + xsmall={ + <MenuItemLink + to="/configuration" + primaryText={translate('pos.configuration')} + leftIcon={<SettingsIcon />} + onClick={onMenuClick} + /> + } + medium={null} + /> + <Responsive + small={logout} + medium={null} // Pass null to render nothing on larger devices + /> + </div> + ); +}; + +Menu.propTypes = { + onMenuClick: PropTypes.func, + logout: PropTypes.object, +}; const mapStateToProps = state => ({ open: state.admin.ui.sidebarOpen, @@ -153,8 +151,7 @@ const enhance = compose( connect( mapStateToProps, {} - ), - translate + ) ); export default enhance(Menu); diff --git a/examples/demo/src/layout/SubMenu.js b/examples/demo/src/layout/SubMenu.js index e30809474c1..38667ad515b 100644 --- a/examples/demo/src/layout/SubMenu.js +++ b/examples/demo/src/layout/SubMenu.js @@ -1,5 +1,4 @@ import React, { Fragment } from 'react'; -import compose from 'recompose/compose'; import ExpandMore from '@material-ui/icons/ExpandMore'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; @@ -9,7 +8,7 @@ import Divider from '@material-ui/core/Divider'; import Collapse from '@material-ui/core/Collapse'; import { withStyles } from '@material-ui/core/styles'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; const styles = { listItem: { @@ -37,44 +36,41 @@ const SubMenu = ({ icon, classes, children, - translate, -}) => ( - <Fragment> - <ListItem - dense - button - onClick={handleToggle} - className={classes.listItem} - > - <ListItemIcon>{isOpen ? <ExpandMore /> : icon}</ListItemIcon> - <ListItemText - inset - primary={isOpen ? translate(name) : ''} - secondary={isOpen ? '' : translate(name)} - className={classes.listItemText} - /> - </ListItem> - <Collapse in={isOpen} timeout="auto" unmountOnExit> - <List +}) => { + const translate = useTranslate(); + return ( + <Fragment> + <ListItem dense - component="div" - disablePadding - className={ - sidebarIsOpen - ? classes.sidebarIsOpen - : classes.sidebarIsClosed - } + button + onClick={handleToggle} + className={classes.listItem} > - {children} - </List> - <Divider /> - </Collapse> - </Fragment> -); - -const enhance = compose( - withStyles(styles), - translate -); + <ListItemIcon>{isOpen ? <ExpandMore /> : icon}</ListItemIcon> + <ListItemText + inset + primary={isOpen ? translate(name) : ''} + secondary={isOpen ? '' : translate(name)} + className={classes.listItemText} + /> + </ListItem> + <Collapse in={isOpen} timeout="auto" unmountOnExit> + <List + dense + component="div" + disablePadding + className={ + sidebarIsOpen + ? classes.sidebarIsOpen + : classes.sidebarIsClosed + } + > + {children} + </List> + <Divider /> + </Collapse> + </Fragment> + ); +}; -export default enhance(SubMenu); +export default withStyles(styles)(SubMenu); diff --git a/examples/demo/src/orders/MobileGrid.js b/examples/demo/src/orders/MobileGrid.js index 674100433dc..45d42910b0a 100644 --- a/examples/demo/src/orders/MobileGrid.js +++ b/examples/demo/src/orders/MobileGrid.js @@ -1,6 +1,5 @@ // in src/comments.js import React from 'react'; -import compose from 'recompose/compose'; import Card from '@material-ui/core/Card'; import CardHeader from '@material-ui/core/CardHeader'; import CardContent from '@material-ui/core/CardContent'; @@ -8,10 +7,10 @@ import { withStyles } from '@material-ui/core/styles'; import { DateField, EditButton, - translate, NumberField, TextField, BooleanField, + useTranslate, } from 'react-admin'; import CustomerReferenceField from '../visitors/CustomerReferenceField'; @@ -38,72 +37,79 @@ const listStyles = theme => ({ }, }); -const MobileGrid = ({ classes, ids, data, basePath, translate }) => ( - <div style={{ margin: '1em' }}> - {ids.map(id => ( - <Card key={id} className={classes.card}> - <CardHeader - title={ - <div className={classes.cardTitleContent}> - <span> - {translate('resources.commands.name', 1)}:  - <TextField +const MobileGrid = ({ classes, ids, data, basePath }) => { + const translate = useTranslate(); + return ( + <div style={{ margin: '1em' }}> + {ids.map(id => ( + <Card key={id} className={classes.card}> + <CardHeader + title={ + <div className={classes.cardTitleContent}> + <span> + {translate('resources.commands.name', 1)} + :  + <TextField + record={data[id]} + source="reference" + /> + </span> + <EditButton + resource="commands" + basePath={basePath} record={data[id]} - source="reference" /> - </span> - <EditButton - resource="commands" + </div> + } + /> + <CardContent className={classes.cardContent}> + <span className={classes.cardContentRow}> + {translate('resources.customers.name', 1)}:  + <CustomerReferenceField + record={data[id]} basePath={basePath} + /> + </span> + <span className={classes.cardContentRow}> + {translate('resources.reviews.fields.date')}:  + <DateField + record={data[id]} + source="date" + showTime + /> + </span> + <span className={classes.cardContentRow}> + {translate( + 'resources.commands.fields.basket.total' + )} + :  + <NumberField record={data[id]} + source="total" + options={{ style: 'currency', currency: 'USD' }} + className={classes.total} /> - </div> - } - /> - <CardContent className={classes.cardContent}> - <span className={classes.cardContentRow}> - {translate('resources.customers.name', 1)}:  - <CustomerReferenceField - record={data[id]} - basePath={basePath} - /> - </span> - <span className={classes.cardContentRow}> - {translate('resources.reviews.fields.date')}:  - <DateField record={data[id]} source="date" showTime /> - </span> - <span className={classes.cardContentRow}> - {translate('resources.commands.fields.basket.total')} - :  - <NumberField - record={data[id]} - source="total" - options={{ style: 'currency', currency: 'USD' }} - className={classes.total} - /> - </span> - <span className={classes.cardContentRow}> - {translate('resources.commands.fields.status')}:  - <TextField source="status" record={data[id]} /> - </span> - <span className={classes.cardContentRow}> - {translate('resources.commands.fields.returned')}:  - <BooleanField record={data[id]} source="returned" /> - </span> - </CardContent> - </Card> - ))} - </div> -); + </span> + <span className={classes.cardContentRow}> + {translate('resources.commands.fields.status')} + :  + <TextField source="status" record={data[id]} /> + </span> + <span className={classes.cardContentRow}> + {translate('resources.commands.fields.returned')} + :  + <BooleanField record={data[id]} source="returned" /> + </span> + </CardContent> + </Card> + ))} + </div> + ); +}; MobileGrid.defaultProps = { data: {}, ids: [], }; -const enhance = compose( - withStyles(listStyles), - translate -); - -export default enhance(MobileGrid); +export default withStyles(listStyles)(MobileGrid); diff --git a/examples/demo/src/orders/OrderEdit.js b/examples/demo/src/orders/OrderEdit.js index fbe14feafb6..14ced2caf15 100644 --- a/examples/demo/src/orders/OrderEdit.js +++ b/examples/demo/src/orders/OrderEdit.js @@ -1,6 +1,5 @@ import React from 'react'; import { - translate, AutocompleteInput, BooleanInput, DateInput, @@ -8,16 +7,22 @@ import { ReferenceInput, SelectInput, SimpleForm, + useTranslate, } from 'react-admin'; import withStyles from '@material-ui/core/styles/withStyles'; import Basket from './Basket'; -const OrderTitle = translate(({ record, translate }) => ( - <span> - {translate('resources.commands.title', { reference: record.reference })} - </span> -)); +const OrderTitle = ({ record }) => { + const translate = useTranslate(); + return ( + <span> + {translate('resources.commands.title', { + reference: record.reference, + })} + </span> + ); +}; const editStyles = { root: { alignItems: 'flex-start' }, diff --git a/examples/demo/src/products/ProductList.js b/examples/demo/src/products/ProductList.js index 2adf807dba0..27f9f243bfa 100644 --- a/examples/demo/src/products/ProductList.js +++ b/examples/demo/src/products/ProductList.js @@ -1,12 +1,12 @@ import React from 'react'; import { - translate, Filter, List, NumberInput, ReferenceInput, SearchInput, SelectInput, + useTranslate, } from 'react-admin'; import Chip from '@material-ui/core/Chip'; import withStyles from '@material-ui/core/styles/withStyles'; @@ -18,11 +18,10 @@ const quickFilterStyles = { }, }; -const QuickFilter = translate( - withStyles(quickFilterStyles)(({ classes, label, translate }) => ( - <Chip className={classes.root} label={translate(label)} /> - )) -); +const QuickFilter = withStyles(quickFilterStyles)(({ classes, label }) => { + const translate = useTranslate(); + return <Chip className={classes.root} label={translate(label)} />; +}); export const ProductFilter = props => ( <Filter {...props}> diff --git a/examples/demo/src/reviews/AcceptButton.js b/examples/demo/src/reviews/AcceptButton.js index cc0f8f44e53..106320cd3c0 100644 --- a/examples/demo/src/reviews/AcceptButton.js +++ b/examples/demo/src/reviews/AcceptButton.js @@ -4,8 +4,7 @@ import { connect } from 'react-redux'; import { formValueSelector } from 'redux-form'; import Button from '@material-ui/core/Button'; import ThumbUp from '@material-ui/icons/ThumbUp'; -import { translate, useMutation } from 'react-admin'; -import compose from 'recompose/compose'; +import { useTranslate, useMutation } from 'react-admin'; const sideEffects = { undoable: true, @@ -27,7 +26,8 @@ const sideEffects = { /** * This custom button demonstrate using useMutation to update data */ -const AcceptButton = ({ record, translate }) => { +const AcceptButton = ({ record }) => { + const translate = useTranslate(); const [approve, { loading }] = useMutation( 'UPDATE', 'reviews', @@ -56,16 +56,10 @@ const AcceptButton = ({ record, translate }) => { AcceptButton.propTypes = { record: PropTypes.object, comment: PropTypes.string, - translate: PropTypes.func, }; const selector = formValueSelector('record-form'); -const enhance = compose( - translate, - connect(state => ({ - comment: selector(state, 'comment'), - })) -); - -export default enhance(AcceptButton); +export default connect(state => ({ + comment: selector(state, 'comment'), +}))(AcceptButton); diff --git a/examples/demo/src/reviews/RejectButton.js b/examples/demo/src/reviews/RejectButton.js index cd3bea6bdff..8df5bca2bca 100644 --- a/examples/demo/src/reviews/RejectButton.js +++ b/examples/demo/src/reviews/RejectButton.js @@ -1,62 +1,52 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { formValueSelector } from 'redux-form'; import Button from '@material-ui/core/Button'; import ThumbDown from '@material-ui/icons/ThumbDown'; -import { translate } from 'react-admin'; -import compose from 'recompose/compose'; +import { useTranslate } from 'react-admin'; import { reviewReject as reviewRejectAction } from './reviewActions'; /** * This custom button demonstrate using a custom action to update data */ -class AcceptButton extends Component { - handleApprove = () => { - const { reviewReject, record, comment } = this.props; +const AcceptButton = ({ record, reviewReject, comment }) => { + const translate = useTranslate(); + const handleApprove = () => { reviewReject(record.id, { ...record, comment }); }; - render() { - const { record, translate } = this.props; - return record && record.status === 'pending' ? ( - <Button - variant="outlined" + return record && record.status === 'pending' ? ( + <Button + variant="outlined" + color="primary" + size="small" + onClick={handleApprove} + > + <ThumbDown color="primary" - size="small" - onClick={this.handleApprove} - > - <ThumbDown - color="primary" - style={{ paddingRight: '0.5em', color: 'red' }} - /> - {translate('resources.reviews.action.reject')} - </Button> - ) : ( - <span /> - ); - } -} + style={{ paddingRight: '0.5em', color: 'red' }} + /> + {translate('resources.reviews.action.reject')} + </Button> + ) : ( + <span /> + ); +}; AcceptButton.propTypes = { comment: PropTypes.string, record: PropTypes.object, reviewReject: PropTypes.func, - translate: PropTypes.func, }; const selector = formValueSelector('record-form'); -const enhance = compose( - translate, - connect( - state => ({ - comment: selector(state, 'comment'), - }), - { - reviewReject: reviewRejectAction, - } - ) -); - -export default enhance(AcceptButton); +export default connect( + state => ({ + comment: selector(state, 'comment'), + }), + { + reviewReject: reviewRejectAction, + } +)(AcceptButton); diff --git a/examples/demo/src/segments/LinkToRelatedCustomers.js b/examples/demo/src/segments/LinkToRelatedCustomers.js index 1fff7c29cb5..b026e00ada4 100644 --- a/examples/demo/src/segments/LinkToRelatedCustomers.js +++ b/examples/demo/src/segments/LinkToRelatedCustomers.js @@ -1,9 +1,8 @@ import React from 'react'; -import compose from 'recompose/compose'; import Button from '@material-ui/core/Button'; import { withStyles } from '@material-ui/core/styles'; import { Link } from 'react-router-dom'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; import { stringify } from 'query-string'; import visitors from '../visitors'; @@ -16,28 +15,27 @@ const styles = { }, }; -const LinkToRelatedCustomers = ({ classes, segment, translate }) => ( - <Button - size="small" - color="primary" - component={Link} - to={{ - pathname: '/customers', - search: stringify({ - page: 1, - perPage: 25, - filter: JSON.stringify({ groups: segment }), - }), - }} - className={classes.link} - > - <visitors.icon className={classes.icon} /> - {translate('resources.segments.fields.customers')} - </Button> -); +const LinkToRelatedCustomers = ({ classes, segment }) => { + const translate = useTranslate(); + return ( + <Button + size="small" + color="primary" + component={Link} + to={{ + pathname: '/customers', + search: stringify({ + page: 1, + perPage: 25, + filter: JSON.stringify({ groups: segment }), + }), + }} + className={classes.link} + > + <visitors.icon className={classes.icon} /> + {translate('resources.segments.fields.customers')} + </Button> + ); +}; -const enhance = compose( - withStyles(styles), - translate -); -export default enhance(LinkToRelatedCustomers); +export default withStyles(styles)(LinkToRelatedCustomers); diff --git a/examples/demo/src/segments/Segments.js b/examples/demo/src/segments/Segments.js index ac9fb631dfa..18cbfe07e04 100644 --- a/examples/demo/src/segments/Segments.js +++ b/examples/demo/src/segments/Segments.js @@ -5,35 +5,38 @@ import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; -import { translate, Title } from 'react-admin'; +import { useTranslate, Title } from 'react-admin'; import LinkToRelatedCustomers from './LinkToRelatedCustomers'; import segments from './data'; -const Segments = ({ translate }) => ( - <Card> - <Title title={translate('resources.segments.name')} /> - <Table> - <TableHead> - <TableRow> - <TableCell> - {translate('resources.segments.fields.name')} - </TableCell> - <TableCell /> - </TableRow> - </TableHead> - <TableBody> - {segments.map(segment => ( - <TableRow key={segment.id}> - <TableCell>{translate(segment.name)}</TableCell> +const Segments = () => { + const translate = useTranslate(); + return ( + <Card> + <Title title={translate('resources.segments.name')} /> + <Table> + <TableHead> + <TableRow> <TableCell> - <LinkToRelatedCustomers segment={segment.id} /> + {translate('resources.segments.fields.name')} </TableCell> + <TableCell /> </TableRow> - ))} - </TableBody> - </Table> - </Card> -); + </TableHead> + <TableBody> + {segments.map(segment => ( + <TableRow key={segment.id}> + <TableCell>{translate(segment.name)}</TableCell> + <TableCell> + <LinkToRelatedCustomers segment={segment.id} /> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </Card> + ); +}; -export default translate(Segments); +export default Segments; diff --git a/examples/demo/src/visitors/MobileGrid.js b/examples/demo/src/visitors/MobileGrid.js index 70b1c2a65c0..6b17ce2768f 100644 --- a/examples/demo/src/visitors/MobileGrid.js +++ b/examples/demo/src/visitors/MobileGrid.js @@ -1,17 +1,16 @@ // in src/comments.js import React from 'react'; -import compose from 'recompose/compose'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import { withStyles } from '@material-ui/core/styles'; -import { DateField, EditButton, translate, NumberField } from 'react-admin'; +import { DateField, EditButton, useTranslate, NumberField } from 'react-admin'; import AvatarField from './AvatarField'; import ColoredNumberField from './ColoredNumberField'; import SegmentsField from './SegmentsField'; -const listStyles = theme => ({ +const styles = theme => ({ card: { height: '100%', display: 'flex', @@ -31,76 +30,78 @@ const listStyles = theme => ({ }, }); -const MobileGrid = ({ classes, ids, data, basePath, translate }) => ( - <div style={{ margin: '1em' }}> - {ids.map(id => ( - <Card key={id} className={classes.card}> - <CardHeader - title={ - <div className={classes.cardTitleContent}> - <h2>{`${data[id].first_name} ${ - data[id].last_name - }`}</h2> - <EditButton - resource="visitors" - basePath={basePath} +const MobileGrid = ({ classes, ids, data, basePath }) => { + const translate = useTranslate(); + return ( + <div style={{ margin: '1em' }}> + {ids.map(id => ( + <Card key={id} className={classes.card}> + <CardHeader + title={ + <div className={classes.cardTitleContent}> + <h2>{`${data[id].first_name} ${ + data[id].last_name + }`}</h2> + <EditButton + resource="visitors" + basePath={basePath} + record={data[id]} + /> + </div> + } + avatar={<AvatarField record={data[id]} size="45" />} + /> + <CardContent className={classes.cardContent}> + <div> + {translate( + 'resources.customers.fields.last_seen_gte' + )} +   + <DateField record={data[id]} + source="last_seen" + type="date" + /> + </div> + <div> + {translate( + 'resources.commands.name', + parseInt(data[id].nb_commands, 10) || 1 + )} +  :  + <NumberField + record={data[id]} + source="nb_commands" + label="resources.customers.fields.commands" + className={classes.nb_commands} + /> + </div> + <div> + {translate( + 'resources.customers.fields.total_spent' + )} +   :{' '} + <ColoredNumberField + record={data[id]} + source="total_spent" + options={{ style: 'currency', currency: 'USD' }} /> </div> - } - avatar={<AvatarField record={data[id]} size="45" />} - /> - <CardContent className={classes.cardContent}> - <div> - {translate('resources.customers.fields.last_seen_gte')} -   - <DateField - record={data[id]} - source="last_seen" - type="date" - /> - </div> - <div> - {translate( - 'resources.commands.name', - parseInt(data[id].nb_commands, 10) || 1 - )} -  :  - <NumberField - record={data[id]} - source="nb_commands" - label="resources.customers.fields.commands" - className={classes.nb_commands} - /> - </div> - <div> - {translate('resources.customers.fields.total_spent')} -   :{' '} - <ColoredNumberField - record={data[id]} - source="total_spent" - options={{ style: 'currency', currency: 'USD' }} - /> - </div> - </CardContent> - {data[id].groups && data[id].groups.length > 0 && ( - <CardContent className={classes.cardContent}> - <SegmentsField record={data[id]} /> </CardContent> - )} - </Card> - ))} - </div> -); + {data[id].groups && data[id].groups.length > 0 && ( + <CardContent className={classes.cardContent}> + <SegmentsField record={data[id]} /> + </CardContent> + )} + </Card> + ))} + </div> + ); +}; MobileGrid.defaultProps = { data: {}, ids: [], }; -const enhance = compose( - withStyles(listStyles), - translate -); - -export default enhance(MobileGrid); +export default withStyles(styles)(MobileGrid); diff --git a/examples/demo/src/visitors/SegmentInput.js b/examples/demo/src/visitors/SegmentInput.js index ce75f51bbcc..736096bde4d 100644 --- a/examples/demo/src/visitors/SegmentInput.js +++ b/examples/demo/src/visitors/SegmentInput.js @@ -1,7 +1,6 @@ import React from 'react'; -import { translate, SelectInput } from 'react-admin'; +import { useTranslate, SelectInput } from 'react-admin'; import withStyles from '@material-ui/core/styles/withStyles'; -import compose from 'recompose/compose'; import segments from '../segments/data'; @@ -9,21 +8,21 @@ const styles = { input: { width: 150 }, }; -const SegmentInput = ({ classes, translate, ...rest }) => ( - <SelectInput - {...rest} - choices={segments.map(segment => ({ - id: segment.id, - name: translate(segment.name), - }))} - className={classes.input} - /> -); +const SegmentInput = ({ classes, ...rest }) => { + const translate = useTranslate(); + return ( + <SelectInput + {...rest} + choices={segments.map(segment => ({ + id: segment.id, + name: translate(segment.name), + }))} + className={classes.input} + /> + ); +}; -const TranslatedSegmentInput = compose( - translate, - withStyles(styles) -)(SegmentInput); +const TranslatedSegmentInput = withStyles(styles)(SegmentInput); TranslatedSegmentInput.defaultProps = { source: 'groups', diff --git a/examples/demo/src/visitors/SegmentsField.js b/examples/demo/src/visitors/SegmentsField.js index ff1da7aa67d..c4d4473aa7e 100644 --- a/examples/demo/src/visitors/SegmentsField.js +++ b/examples/demo/src/visitors/SegmentsField.js @@ -1,6 +1,6 @@ import React from 'react'; import Chip from '@material-ui/core/Chip'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; import segments from '../segments/data'; const styles = { @@ -8,24 +8,27 @@ const styles = { chip: { margin: 4 }, }; -const SegmentsField = ({ record, translate }) => ( - <span style={styles.main}> - {record.groups && - record.groups.map(segment => ( - <Chip - key={segment} - style={styles.chip} - label={translate(segments.find(s => s.id === segment).name)} - /> - ))} - </span> -); - -const TranslatedSegmentsField = translate(SegmentsField); +const SegmentsField = ({ record }) => { + const translate = useTranslate(); + return ( + <span style={styles.main}> + {record.groups && + record.groups.map(segment => ( + <Chip + key={segment} + style={styles.chip} + label={translate( + segments.find(s => s.id === segment).name + )} + /> + ))} + </span> + ); +}; -TranslatedSegmentsField.defaultProps = { +SegmentsField.defaultProps = { addLabel: true, source: 'groups', }; -export default TranslatedSegmentsField; +export default SegmentsField; diff --git a/examples/demo/src/visitors/SegmentsInput.js b/examples/demo/src/visitors/SegmentsInput.js index 628065602b8..62e4d8d947b 100644 --- a/examples/demo/src/visitors/SegmentsInput.js +++ b/examples/demo/src/visitors/SegmentsInput.js @@ -1,23 +1,24 @@ import React from 'react'; -import { translate, SelectArrayInput } from 'react-admin'; +import { useTranslate, SelectArrayInput } from 'react-admin'; import segments from '../segments/data'; -const SegmentsInput = ({ translate, addField, ...rest }) => ( - <SelectArrayInput - {...rest} - choices={segments.map(segment => ({ - id: segment.id, - name: translate(segment.name), - }))} - /> -); - -const TranslatedSegmentsInput = translate(SegmentsInput); +const SegmentsInput = ({ addField, ...rest }) => { + const translate = useTranslate(); + return ( + <SelectArrayInput + {...rest} + choices={segments.map(segment => ({ + id: segment.id, + name: translate(segment.name), + }))} + /> + ); +}; -TranslatedSegmentsInput.defaultProps = { +SegmentsInput.defaultProps = { addField: true, source: 'groups', }; -export default TranslatedSegmentsInput; +export default SegmentsInput; diff --git a/examples/simple/src/comments/CommentList.js b/examples/simple/src/comments/CommentList.js index f2d48e5ef86..8701d43d979 100644 --- a/examples/simple/src/comments/CommentList.js +++ b/examples/simple/src/comments/CommentList.js @@ -27,7 +27,7 @@ import { SimpleList, TextField, downloadCSV, - translate, + useTranslate, } from 'react-admin'; // eslint-disable-line import/no-unresolved const CommentFilter = props => ( @@ -58,42 +58,48 @@ const exporter = (records, fetchRelatedRecords) => downloadCSV(convertToCSV({ data, fields }), 'comments'); }); -const CommentPagination = translate( - ({ isLoading, ids, page, perPage, total, setPage, translate }) => { - const nbPages = Math.ceil(total / perPage) || 1; - if (!isLoading && (total === 0 || (ids && !ids.length))) { - return <PaginationLimit total={total} page={page} ids={ids} />; - } - - return ( - nbPages > 1 && ( - <Toolbar> - {page > 1 && ( - <Button - color="primary" - key="prev" - onClick={() => setPage(page - 1)} - > - <ChevronLeft /> -   - {translate('ra.navigation.prev')} - </Button> - )} - {page !== nbPages && ( - <Button - color="primary" - key="next" - onClick={() => setPage(page + 1)} - > - {translate('ra.navigation.next')}  - <ChevronRight /> - </Button> - )} - </Toolbar> - ) - ); +const CommentPagination = ({ + isLoading, + ids, + page, + perPage, + total, + setPage, +}) => { + const translate = useTranslate(); + const nbPages = Math.ceil(total / perPage) || 1; + if (!isLoading && (total === 0 || (ids && !ids.length))) { + return <PaginationLimit total={total} page={page} ids={ids} />; } -); + + return ( + nbPages > 1 && ( + <Toolbar> + {page > 1 && ( + <Button + color="primary" + key="prev" + onClick={() => setPage(page - 1)} + > + <ChevronLeft /> +   + {translate('ra.navigation.prev')} + </Button> + )} + {page !== nbPages && ( + <Button + color="primary" + key="next" + onClick={() => setPage(page + 1)} + > + {translate('ra.navigation.next')}  + <ChevronRight /> + </Button> + )} + </Toolbar> + ) + ); +}; const listStyles = theme => ({ card: { @@ -115,66 +121,69 @@ const listStyles = theme => ({ }); const CommentGrid = withStyles(listStyles)( - translate(({ classes, ids, data, basePath, translate }) => ( - <Grid spacing={16} container style={{ padding: '0 1em' }}> - {ids.map(id => ( - <Grid item key={id} sm={12} md={6} lg={4}> - <Card className={classes.card}> - <CardHeader - className="comment" - title={ - <TextField + ({ classes, ids, data, basePath }) => { + const translate = useTranslate(); + return ( + <Grid spacing={16} container style={{ padding: '0 1em' }}> + {ids.map(id => ( + <Grid item key={id} sm={12} md={6} lg={4}> + <Card className={classes.card}> + <CardHeader + className="comment" + title={ + <TextField + record={data[id]} + source="author.name" + /> + } + subheader={ + <DateField + record={data[id]} + source="created_at" + /> + } + avatar={ + <Avatar> + <PersonIcon /> + </Avatar> + } + /> + <CardContent className={classes.cardContent}> + <TextField record={data[id]} source="body" /> + </CardContent> + <CardContent className={classes.cardLink}> + {translate('comment.list.about')}  + <ReferenceField + resource="comments" record={data[id]} - source="author.name" - /> - } - subheader={ - <DateField + source="post_id" + reference="posts" + basePath={basePath} + > + <TextField + source="title" + className={classes.cardLinkLink} + /> + </ReferenceField> + </CardContent> + <CardActions className={classes.cardActions}> + <EditButton + resource="posts" + basePath={basePath} record={data[id]} - source="created_at" /> - } - avatar={ - <Avatar> - <PersonIcon /> - </Avatar> - } - /> - <CardContent className={classes.cardContent}> - <TextField record={data[id]} source="body" /> - </CardContent> - <CardContent className={classes.cardLink}> - {translate('comment.list.about')}  - <ReferenceField - resource="comments" - record={data[id]} - source="post_id" - reference="posts" - basePath={basePath} - > - <TextField - source="title" - className={classes.cardLinkLink} + <ShowButton + resource="posts" + basePath={basePath} + record={data[id]} /> - </ReferenceField> - </CardContent> - <CardActions className={classes.cardActions}> - <EditButton - resource="posts" - basePath={basePath} - record={data[id]} - /> - <ShowButton - resource="posts" - basePath={basePath} - record={data[id]} - /> - </CardActions> - </Card> - </Grid> - ))} - </Grid> - )) + </CardActions> + </Card> + </Grid> + ))} + </Grid> + ); + } ); CommentGrid.defaultProps = { diff --git a/examples/simple/src/comments/PostQuickCreateCancelButton.js b/examples/simple/src/comments/PostQuickCreateCancelButton.js index 2b0f2814126..f8c3b306946 100644 --- a/examples/simple/src/comments/PostQuickCreateCancelButton.js +++ b/examples/simple/src/comments/PostQuickCreateCancelButton.js @@ -1,12 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import compose from 'recompose/compose'; import Button from '@material-ui/core/Button'; import IconCancel from '@material-ui/icons/Cancel'; import withStyles from '@material-ui/core/styles/withStyles'; -import { translate } from 'react-admin'; // eslint-disable-line import/no-unresolved +import { useTranslate } from 'react-admin'; const styles = { button: { @@ -18,26 +17,20 @@ const styles = { }, }; -const CancelButtonView = ({ - classes, - onClick, - label = 'ra.action.cancel', - translate, -}) => ( - <Button className={classes.button} onClick={onClick}> - <IconCancel className={classes.iconPaddingStyle} /> - {label && translate(label, { _: label })} - </Button> -); +const CancelButtonView = ({ classes, onClick, label = 'ra.action.cancel' }) => { + const translate = useTranslate(); + return ( + <Button className={classes.button} onClick={onClick}> + <IconCancel className={classes.iconPaddingStyle} /> + {label && translate(label, { _: label })} + </Button> + ); +}; CancelButtonView.propTypes = { classes: PropTypes.object, label: PropTypes.string, onClick: PropTypes.func.isRequired, - translate: PropTypes.func.isRequired, }; -export default compose( - translate, - withStyles(styles) -)(CancelButtonView); +export default withStyles(styles)(CancelButtonView); diff --git a/examples/simple/src/posts/PostList.js b/examples/simple/src/posts/PostList.js index dcfd0ac2ab5..d7d8500b610 100644 --- a/examples/simple/src/posts/PostList.js +++ b/examples/simple/src/posts/PostList.js @@ -20,15 +20,16 @@ import { SingleFieldList, TextField, TextInput, - translate, + useTranslate, } from 'react-admin'; // eslint-disable-line import/no-unresolved import ResetViewsButton from './ResetViewsButton'; export const PostIcon = BookIcon; -const QuickFilter = translate(({ label, translate }) => ( - <Chip style={{ marginBottom: 8 }} label={translate(label)} /> -)); +const QuickFilter = ({ label }) => { + const translate = useTranslate(); + return <Chip style={{ marginBottom: 8 }} label={translate(label)} />; +}; const PostFilter = props => ( <Filter {...props}> diff --git a/examples/simple/src/posts/PostTitle.js b/examples/simple/src/posts/PostTitle.js index 5fdbdd3dfc7..5f3f58c5256 100644 --- a/examples/simple/src/posts/PostTitle.js +++ b/examples/simple/src/posts/PostTitle.js @@ -1,8 +1,13 @@ import React from 'react'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; -export default translate(({ record, translate }) => ( - <span> - {record ? translate('post.edit.title', { title: record.title }) : ''} - </span> -)); +export default ({ record }) => { + const translate = useTranslate(); + return ( + <span> + {record + ? translate('post.edit.title', { title: record.title }) + : ''} + </span> + ); +}; diff --git a/examples/simple/src/users/UserTitle.js b/examples/simple/src/users/UserTitle.js index b907734490f..be6e71c6d80 100644 --- a/examples/simple/src/users/UserTitle.js +++ b/examples/simple/src/users/UserTitle.js @@ -1,11 +1,14 @@ /* eslint react/jsx-key: off */ import React from 'react'; -import { translate } from 'react-admin'; +import { useTranslate } from 'react-admin'; -const UserTitle = translate(({ record, translate }) => ( - <span> - {record ? translate('user.edit.title', { title: record.name }) : ''} - </span> -)); +const UserTitle = ({ record }) => { + const translate = useTranslate(); + return ( + <span> + {record ? translate('user.edit.title', { title: record.name }) : ''} + </span> + ); +}; export default UserTitle; diff --git a/packages/ra-core/src/i18n/index.ts b/packages/ra-core/src/i18n/index.ts index 8f02471aa23..fc22ee10b32 100644 --- a/packages/ra-core/src/i18n/index.ts +++ b/packages/ra-core/src/i18n/index.ts @@ -1,11 +1,18 @@ import defaultI18nProvider from './defaultI18nProvider'; import translate from './translate'; import TranslationProvider from './TranslationProvider'; +import useTranslate from './useTranslate'; -// Alias to translate to avoid shadowed variable names error with tsling +// Alias to translate to avoid shadowed variable names error with tslint const withTranslate = translate; -export { defaultI18nProvider, translate, withTranslate, TranslationProvider }; +export { + defaultI18nProvider, + translate, + withTranslate, + useTranslate, + TranslationProvider, +}; export const DEFAULT_LOCALE = 'en'; export * from './TranslationUtils'; diff --git a/packages/ra-core/src/i18n/useTranslate.spec.tsx b/packages/ra-core/src/i18n/useTranslate.spec.tsx new file mode 100644 index 00000000000..eaad1d0163f --- /dev/null +++ b/packages/ra-core/src/i18n/useTranslate.spec.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import expect from 'expect'; +import { render, cleanup } from 'react-testing-library'; + +import useTranslate from './useTranslate'; +import { TranslationContext } from './TranslationContext'; +import { TestContext } from '../util'; + +describe('useTranslate', () => { + afterEach(cleanup); + + const Component = () => { + const translate = useTranslate(); + return <div>{translate('hello')}</div>; + }; + + it('should not fail when used outside of a translation provider', () => { + const { queryAllByText } = render(<Component />); + expect(queryAllByText('hello')).toHaveLength(1); + }); + + it('should use the translate function set in the translation context', () => { + const { queryAllByText } = render( + <TranslationContext.Provider + value={{ locale: 'de', translate: () => 'hallo' }} + > + <Component /> + </TranslationContext.Provider> + ); + expect(queryAllByText('hello')).toHaveLength(0); + expect(queryAllByText('hallo')).toHaveLength(1); + }); + + it('should use the messages set in the store', () => { + const { queryAllByText } = render( + <TestContext store={{ i18n: { messages: { hello: 'bonjour' } } }}> + <Component /> + </TestContext> + ); + expect(queryAllByText('hello')).toHaveLength(0); + expect(queryAllByText('bonjour')).toHaveLength(1); + }); +}); diff --git a/packages/ra-core/src/i18n/useTranslate.ts b/packages/ra-core/src/i18n/useTranslate.ts new file mode 100644 index 00000000000..574e74a692a --- /dev/null +++ b/packages/ra-core/src/i18n/useTranslate.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; + +import { TranslationContext } from './TranslationContext'; + +const useTranslate = () => { + const { translate } = useContext(TranslationContext); + return translate; +}; + +export default useTranslate; diff --git a/packages/ra-core/src/util/FieldTitle.spec.tsx b/packages/ra-core/src/util/FieldTitle.spec.tsx index 0569687fec7..2a6379ee41e 100644 --- a/packages/ra-core/src/util/FieldTitle.spec.tsx +++ b/packages/ra-core/src/util/FieldTitle.spec.tsx @@ -1,88 +1,93 @@ import assert from 'assert'; +import expect from 'expect'; import { shallow } from 'enzyme'; +import { render, cleanup } from 'react-testing-library'; import React from 'react'; import { FieldTitle } from './FieldTitle'; +import TestContext from './TestContext'; describe('FieldTitle', () => { + afterEach(cleanup); + const translateMock = dictionary => (term, options) => dictionary[term] || options._ || ''; - it('should return empty span by default', () => - assert.equal(shallow(<FieldTitle />).html(), '<span></span>')); - it('should use the label when given', () => - assert.equal( - shallow(<FieldTitle label="foo" />).html(), - '<span>foo</span>' - )); - it('should the label as translate key when translation is available', () => - assert.equal( - shallow( - <FieldTitle - label="foo" - translate={translateMock({ foo: 'bar' })} - /> - ).html(), - '<span>bar</span>' - )); + + it('should return empty span by default', () => { + const { container } = render(<FieldTitle />); + expect(container.firstChild).toBeInstanceOf(HTMLSpanElement); + expect(container.firstChild.textContent).toEqual(''); + }); + + it('should use the label when given', () => { + const { container } = render(<FieldTitle label="foo" />); + expect(container.firstChild.textContent).toEqual('foo'); + }); + + it('should use the label as translate key when translation is available', () => { + const { container } = render( + <TestContext store={{ i18n: { messages: { foo: 'bar' } } }}> + <FieldTitle label="foo" /> + </TestContext> + ); + expect(container.firstChild.textContent).toEqual('bar'); + }); it('should use the humanized source when given', () => { - assert.equal( - shallow( - <FieldTitle - resource="posts" - source="title" - translate={translateMock({})} - /> - ).html(), - '<span>Title</span>' + const { container } = render( + <TestContext> + <FieldTitle resource="posts" source="title" /> + </TestContext> ); + expect(container.firstChild.textContent).toEqual('Title'); + }); - assert.equal( - shallow( - <FieldTitle - resource="posts" - source="title_with_underscore" - translate={translateMock({})} - /> - ).html(), - '<span>Title with underscore</span>' + it('should use the humanized source when given with underscores', () => { + const { container } = render( + <TestContext> + <FieldTitle resource="posts" source="title_with_underscore" /> + </TestContext> + ); + expect(container.firstChild.textContent).toEqual( + 'Title with underscore' ); + }); + + it('should use the humanized source when given with camelCase', () => { + const { container } = render( + <TestContext> + <FieldTitle resource="posts" source="titleWithCamelCase" /> + </TestContext> + ); + expect(container.firstChild.textContent).toEqual( + 'Title with camel case' + ); + }); - assert.equal( - shallow( - <FieldTitle - resource="posts" - source="titleWithCamelCase" - translate={translateMock({})} - /> - ).html(), - '<span>Title with camel case</span>' + it('should use the source and resource as translate key when translation is available', () => { + const { container } = render( + <TestContext + store={{ + i18n: { + messages: { 'resources.posts.fields.title': 'titre' }, + }, + }} + > + <FieldTitle resource="posts" source="title" /> + </TestContext> ); + expect(container.firstChild.textContent).toEqual('titre'); }); - it('should use the source and resource as translate key when translation is available', () => - assert.equal( - shallow( - <FieldTitle - resource="posts" - source="title" - translate={translateMock({ - 'resources.posts.fields.title': 'titre', - })} - /> - ).html(), - '<span>titre</span>' - )); - it('should use label rather than source', () => - assert.equal( - shallow( - <FieldTitle label="foo" resource="posts" source="title" /> - ).html(), - '<span>foo</span>' - )); - it('should add a trailing asterisk if the field is required', () => - assert.equal( - shallow(<FieldTitle label="foo" isRequired />).html(), - '<span>foo *</span>' - )); + it('should use label rather than source', () => { + const { container } = render( + <FieldTitle label="foo" resource="posts" source="title" /> + ); + expect(container.firstChild.textContent).toEqual('foo'); + }); + + it('should add a trailing asterisk if the field is required', () => { + const { container } = render(<FieldTitle label="foo" isRequired />); + expect(container.firstChild.textContent).toEqual('foo *'); + }); }); diff --git a/packages/ra-core/src/util/FieldTitle.tsx b/packages/ra-core/src/util/FieldTitle.tsx index 3f81bed261c..68f04350550 100644 --- a/packages/ra-core/src/util/FieldTitle.tsx +++ b/packages/ra-core/src/util/FieldTitle.tsx @@ -1,18 +1,14 @@ import React, { SFC } from 'react'; -import inflection from 'inflection'; import pure from 'recompose/pure'; -import compose from 'recompose/compose'; -import translateHoc from '../i18n/translate'; +import useTranslate from '../i18n/useTranslate'; import getFieldLabelTranslationArgs from './getFieldLabelTranslationArgs'; -import { Translate } from '../types'; interface Props { isRequired?: boolean; resource?: string; source?: string; label?: string; - translate?: Translate; } export const FieldTitle: SFC<Props> = ({ @@ -20,22 +16,19 @@ export const FieldTitle: SFC<Props> = ({ source, label, isRequired, - translate = (name: string, options) => name, -}) => ( - <span> - {translate( - ...getFieldLabelTranslationArgs({ label, resource, source }) - )} - {isRequired && ' *'} - </span> -); +}) => { + const translate = useTranslate(); + return ( + <span> + {translate( + ...getFieldLabelTranslationArgs({ label, resource, source }) + )} + {isRequired && ' *'} + </span> + ); +}; // wat? TypeScript looses the displayName if we don't set it explicitly FieldTitle.displayName = 'FieldTitle'; -const enhance = compose( - translateHoc, - pure -); - -export default enhance(FieldTitle); +export default pure(FieldTitle); diff --git a/packages/ra-ui-materialui/src/auth/Logout.tsx b/packages/ra-ui-materialui/src/auth/Logout.tsx index 05d143d7b8f..1918a0c8516 100644 --- a/packages/ra-ui-materialui/src/auth/Logout.tsx +++ b/packages/ra-ui-materialui/src/auth/Logout.tsx @@ -11,19 +11,13 @@ import { } from '@material-ui/core/styles'; import ExitIcon from '@material-ui/icons/PowerSettingsNew'; import classnames from 'classnames'; -import { - withTranslate, - userLogout as userLogoutAction, - TranslationContextProps, -} from 'ra-core'; +import { useTranslate, userLogout as userLogoutAction } from 'ra-core'; interface Props { redirectTo?: string; } -interface EnhancedProps - extends TranslationContextProps, - WithStyles<typeof styles> { +interface EnhancedProps extends WithStyles<typeof styles> { userLogout: () => void; } @@ -48,23 +42,24 @@ const styles = (theme: Theme) => const Logout: SFC<Props & EnhancedProps & MenuItemProps> = ({ classes, className, - locale, redirectTo, - translate, userLogout, ...rest -}) => ( - <MenuItem - className={classnames('logout', classes.menuItem, className)} - onClick={userLogout} - {...rest} - > - <span className={classes.iconMenuPaddingStyle}> - <ExitIcon /> - </span> - {translate('ra.auth.logout')} - </MenuItem> -); +}) => { + const translate = useTranslate(); + return ( + <MenuItem + className={classnames('logout', classes.menuItem, className)} + onClick={userLogout} + {...rest} + > + <span className={classes.iconMenuPaddingStyle}> + <ExitIcon /> + </span> + {translate('ra.auth.logout')} + </MenuItem> + ); +}; const mapDispatchToProps = (dispatch, { redirectTo }) => ({ userLogout: () => dispatch(userLogoutAction(redirectTo)), @@ -74,7 +69,6 @@ const enhance = compose< Props & EnhancedProps & MenuItemProps, Props & MenuItemProps >( - withTranslate, connect( undefined, mapDispatchToProps diff --git a/packages/ra-ui-materialui/src/button/Button.js b/packages/ra-ui-materialui/src/button/Button.js index dc8a12815b0..2ca6492b42d 100644 --- a/packages/ra-ui-materialui/src/button/Button.js +++ b/packages/ra-ui-materialui/src/button/Button.js @@ -1,12 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import compose from 'recompose/compose'; import MuiButton from '@material-ui/core/Button'; import Tooltip from '@material-ui/core/Tooltip'; import IconButton from '@material-ui/core/IconButton'; import { withStyles, createStyles } from '@material-ui/core/styles'; import classnames from 'classnames'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; import Responsive from '../layout/Responsive'; @@ -41,66 +40,70 @@ const Button = ({ disabled, label, size, - translate, ...rest -}) => ( - <Responsive - small={ - label && !disabled ? ( - <Tooltip title={translate(label, { _: label })}> +}) => { + const translate = useTranslate(); + return ( + <Responsive + small={ + label && !disabled ? ( + <Tooltip title={translate(label, { _: label })}> + <IconButton + aria-label={translate(label, { _: label })} + className={className} + color={color} + {...rest} + > + {children} + </IconButton> + </Tooltip> + ) : ( <IconButton - aria-label={translate(label, { _: label })} className={className} color={color} + disabled={disabled} {...rest} > {children} </IconButton> - </Tooltip> - ) : ( - <IconButton - className={className} + ) + } + medium={ + <MuiButton + className={classnames(classes.button, className)} color={color} + size={size} + aria-label={ + label ? translate(label, { _: label }) : undefined + } disabled={disabled} {...rest} > - {children} - </IconButton> - ) - } - medium={ - <MuiButton - className={classnames(classes.button, className)} - color={color} - size={size} - aria-label={label ? translate(label, { _: label }) : undefined} - disabled={disabled} - {...rest} - > - {alignIcon === 'left' && - children && - React.cloneElement(children, { - className: classes[`${size}Icon`], - })} - {label && ( - <span - className={classnames({ - [classes.label]: alignIcon === 'left', - [classes.labelRightIcon]: alignIcon !== 'left', + {alignIcon === 'left' && + children && + React.cloneElement(children, { + className: classes[`${size}Icon`], })} - > - {translate(label, { _: label })} - </span> - )} - {alignIcon === 'right' && - children && - React.cloneElement(children, { - className: classes[`${size}Icon`], - })} - </MuiButton> - } - /> -); + {label && ( + <span + className={classnames({ + [classes.label]: alignIcon === 'left', + [classes.labelRightIcon]: alignIcon !== 'left', + })} + > + {translate(label, { _: label })} + </span> + )} + {alignIcon === 'right' && + children && + React.cloneElement(children, { + className: classes[`${size}Icon`], + })} + </MuiButton> + } + /> + ); +}; Button.propTypes = { alignIcon: PropTypes.string, @@ -111,17 +114,11 @@ Button.propTypes = { disabled: PropTypes.bool, label: PropTypes.string, size: PropTypes.oneOf(['small', 'medium', 'large']), - translate: PropTypes.func.isRequired, }; Button.defaultProps = { color: 'primary', - size: 'small' -} - -const enhance = compose( - withStyles(styles), - translate -); + size: 'small', +}; -export default enhance(Button); +export default withStyles(styles)(Button); diff --git a/packages/ra-ui-materialui/src/button/CreateButton.js b/packages/ra-ui-materialui/src/button/CreateButton.js index ede5765b114..e696973a918 100644 --- a/packages/ra-ui-materialui/src/button/CreateButton.js +++ b/packages/ra-ui-materialui/src/button/CreateButton.js @@ -7,7 +7,7 @@ import ContentAdd from '@material-ui/icons/Add'; import compose from 'recompose/compose'; import classnames from 'classnames'; import { Link } from 'react-router-dom'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; import Button from './Button'; import Responsive from '../layout/Responsive'; @@ -33,38 +33,40 @@ const CreateButton = ({ basePath = '', className, classes = {}, - translate, label = 'ra.action.create', icon = <ContentAdd />, ...rest -}) => ( - <Responsive - small={ - <MuiButton - component={Link} - variant="fab" - color="primary" - className={classnames(classes.floating, className)} - to={`${basePath}/create`} - aria-label={label && translate(label)} - {...rest} - > - {icon} - </MuiButton> - } - medium={ - <Button - component={Link} - to={`${basePath}/create`} - className={className} - label={label} - {...rest} - > - {icon} - </Button> - } - /> -); +}) => { + const translate = useTranslate(); + return ( + <Responsive + small={ + <MuiButton + component={Link} + variant="fab" + color="primary" + className={classnames(classes.floating, className)} + to={`${basePath}/create`} + aria-label={label && translate(label)} + {...rest} + > + {icon} + </MuiButton> + } + medium={ + <Button + component={Link} + to={`${basePath}/create`} + className={className} + label={label} + {...rest} + > + {icon} + </Button> + } + /> + ); +}; CreateButton.propTypes = { basePath: PropTypes.string, @@ -72,12 +74,10 @@ CreateButton.propTypes = { classes: PropTypes.object, label: PropTypes.string, size: PropTypes.string, - translate: PropTypes.func.isRequired, icon: PropTypes.element, }; const enhance = compose( - translate, onlyUpdateForKeys(['basePath', 'label', 'translate']), withStyles(styles) ); diff --git a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.js b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.js index dd2041b0306..c8cb8c8c96c 100644 --- a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.js +++ b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.js @@ -27,6 +27,7 @@ export const sanitizeRestProps = ({ undoable, redirect, submitOnEnter, + translate, ...rest }) => rest; diff --git a/packages/ra-ui-materialui/src/field/BooleanField.spec.js b/packages/ra-ui-materialui/src/field/BooleanField.spec.js index 14aed4e9197..8ac734d19f4 100644 --- a/packages/ra-ui-materialui/src/field/BooleanField.spec.js +++ b/packages/ra-ui-materialui/src/field/BooleanField.spec.js @@ -7,16 +7,13 @@ const defaultProps = { record: { published: true }, source: 'published', resource: 'posts', - translate: x => x, - classes: {} + classes: {}, }; describe('<BooleanField />', () => { afterEach(cleanup); it('should display tick and truthy text if value is true', () => { - const { queryByText } = render( - <BooleanField {...defaultProps} /> - ); + const { queryByText } = render(<BooleanField {...defaultProps} />); expect(queryByText('ra.boolean.true')).not.toBeNull(); expect(queryByText('ra.boolean.true').nextSibling.dataset.testid).toBe( 'true' @@ -26,7 +23,7 @@ describe('<BooleanField />', () => { it('should use valueLabelTrue for custom truthy text', () => { const { queryByText } = render( - <BooleanField + <BooleanField {...defaultProps} valueLabelTrue="Has been published" /> diff --git a/packages/ra-ui-materialui/src/field/BooleanField.tsx b/packages/ra-ui-materialui/src/field/BooleanField.tsx index 2718b1a9bff..d83b162a569 100644 --- a/packages/ra-ui-materialui/src/field/BooleanField.tsx +++ b/packages/ra-ui-materialui/src/field/BooleanField.tsx @@ -7,7 +7,7 @@ import TrueIcon from '@material-ui/icons/Done'; import Typography, { TypographyProps } from '@material-ui/core/Typography'; import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles'; import compose from 'recompose/compose'; -import { translate as withTranslate, TranslationContextProps } from 'ra-core'; +import { useTranslate } from 'ra-core'; import { FieldProps, InjectedFieldProps, fieldPropTypes } from './types'; import sanitizeRestProps from './sanitizeRestProps'; @@ -37,9 +37,7 @@ interface Props extends FieldProps { valueLabelFalse?: string; } -interface EnhancedProps - extends TranslationContextProps, - WithStyles<typeof styles> {} +interface EnhancedProps extends WithStyles<typeof styles> {} export const BooleanField: SFC< Props & InjectedFieldProps & EnhancedProps & TypographyProps @@ -48,11 +46,11 @@ export const BooleanField: SFC< classes, source, record = {}, - translate, valueLabelTrue, valueLabelFalse, ...rest }) => { + const translate = useTranslate(); const value = get(record, source); let ariaLabel = value ? valueLabelTrue : valueLabelFalse; @@ -107,8 +105,7 @@ const EnhancedBooleanField = compose< Props & TypographyProps >( pure, - withStyles(styles), - withTranslate + withStyles(styles) )(BooleanField); EnhancedBooleanField.defaultProps = { diff --git a/packages/ra-ui-materialui/src/field/SelectField.spec.js b/packages/ra-ui-materialui/src/field/SelectField.spec.js index dc75ce692b6..e4e4ea72336 100644 --- a/packages/ra-ui-materialui/src/field/SelectField.spec.js +++ b/packages/ra-ui-materialui/src/field/SelectField.spec.js @@ -1,67 +1,68 @@ import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; +import expect from 'expect'; +import { render, cleanup } from 'react-testing-library'; + +import { TestContext } from 'ra-core'; import { SelectField } from './SelectField'; describe('<SelectField />', () => { + afterEach(cleanup); + const defaultProps = { source: 'foo', choices: [{ id: 0, name: 'hello' }, { id: 1, name: 'world' }], - translate: x => x, }; - it('should return null when the record is not set', () => - assert.equal(shallow(<SelectField {...defaultProps} />).html(), null)); + it('should return null when the record is not set', () => { + const { container } = render(<SelectField {...defaultProps} />); + expect(container.firstChild).toBeNull(); + }); - it('should return null when the record has no value for the source', () => - assert.equal( - shallow(<SelectField {...defaultProps} record={{}} />).html(), - null - )); + it('should return null when the record has no value for the source', () => { + const { container } = render( + <SelectField {...defaultProps} record={{}} /> + ); + expect(container.firstChild).toBeNull(); + }); - it('should return null when the record has a value for the source not in the choices', () => - assert.equal( - shallow( - <SelectField {...defaultProps} record={{ foo: 2 }} /> - ).html(), - null - )); + it('should return null when the record has a value for the source not in the choices', () => { + const { container } = render( + <SelectField {...defaultProps} record={{ foo: 2 }} /> + ); + expect(container.firstChild).toBeNull(); + }); it('should render the choice', () => { - const wrapper = shallow( + const { queryAllByText } = render( <SelectField {...defaultProps} record={{ foo: 0 }} /> ); - const chipElement = wrapper.find('WithStyles(Typography)'); - assert.equal(chipElement.children().text(), 'hello'); + expect(queryAllByText('hello')).toHaveLength(1); }); it('should use custom className', () => { - const wrapper = shallow( + const { container } = render( <SelectField {...defaultProps} record={{ foo: 1 }} - elStyle={{ margin: 1 }} - className="foo" + className="lorem" /> ); - const chipElement = wrapper.find('WithStyles(Typography)'); - assert.deepEqual(chipElement.prop('className'), 'foo'); + expect(container.firstChild.className).toContain('lorem'); }); it('should handle deep fields', () => { - const wrapper = shallow( + const { queryAllByText } = render( <SelectField {...defaultProps} - record={{ foo: { bar: 0 } }} source="foo.bar" + record={{ foo: { bar: 0 } }} /> ); - const chipElement = wrapper.find('WithStyles(Typography)'); - assert.equal(chipElement.children().text(), 'hello'); + expect(queryAllByText('hello')).toHaveLength(1); }); it('should use optionValue as value identifier', () => { - const wrapper = shallow( + const { queryAllByText } = render( <SelectField {...defaultProps} record={{ foo: 0 }} @@ -69,12 +70,11 @@ describe('<SelectField />', () => { choices={[{ foobar: 0, name: 'hello' }]} /> ); - const chipElement = wrapper.find('WithStyles(Typography)'); - assert.equal(chipElement.children().text(), 'hello'); + expect(queryAllByText('hello')).toHaveLength(1); }); it('should use optionText with a string value as text identifier', () => { - const wrapper = shallow( + const { queryAllByText } = render( <SelectField {...defaultProps} record={{ foo: 0 }} @@ -82,12 +82,11 @@ describe('<SelectField />', () => { choices={[{ id: 0, foobar: 'hello' }]} /> ); - const chipElement = wrapper.find('WithStyles(Typography)'); - assert.equal(chipElement.children().text(), 'hello'); + expect(queryAllByText('hello')).toHaveLength(1); }); it('should use optionText with a function value as text identifier', () => { - const wrapper = shallow( + const { queryAllByText } = render( <SelectField {...defaultProps} record={{ foo: 0 }} @@ -95,13 +94,12 @@ describe('<SelectField />', () => { choices={[{ id: 0, foobar: 'hello' }]} /> ); - const chipElement = wrapper.find('WithStyles(Typography)'); - assert.equal(chipElement.children().text(), 'hello'); + expect(queryAllByText('hello')).toHaveLength(1); }); it('should use optionText with an element value as text identifier', () => { const Foobar = ({ record }) => <span>{record.foobar}</span>; - const wrapper = shallow( + const { queryAllByText } = render( <SelectField {...defaultProps} record={{ foo: 0 }} @@ -109,35 +107,30 @@ describe('<SelectField />', () => { choices={[{ id: 0, foobar: 'hello' }]} /> ); - const chipElement = wrapper.find('Foobar'); - assert.deepEqual(chipElement.prop('record'), { - id: 0, - foobar: 'hello', - }); + expect(queryAllByText('hello')).toHaveLength(1); }); it('should translate the choice by default', () => { - const wrapper = shallow( - <SelectField - {...defaultProps} - record={{ foo: 0 }} - translate={x => `**${x}**`} - /> + const { queryAllByText } = render( + <TestContext store={{ i18n: { messages: { hello: 'bonjour' } } }}> + <SelectField {...defaultProps} record={{ foo: 0 }} /> + </TestContext> ); - const chipElement = wrapper.find('WithStyles(Typography)'); - assert.equal(chipElement.children().text(), '**hello**'); + expect(queryAllByText('hello')).toHaveLength(0); + expect(queryAllByText('bonjour')).toHaveLength(1); }); it('should not translate the choice if translateChoice is false', () => { - const wrapper = shallow( - <SelectField - {...defaultProps} - record={{ foo: 0 }} - translate={x => `**${x}**`} - translateChoice={false} - /> + const { queryAllByText } = render( + <TestContext store={{ i18n: { messages: { hello: 'bonjour' } } }}> + <SelectField + {...defaultProps} + record={{ foo: 0 }} + translateChoice={false} + /> + </TestContext> ); - const chipElement = wrapper.find('WithStyles(Typography)'); - assert.equal(chipElement.children().text(), 'hello'); + expect(queryAllByText('hello')).toHaveLength(1); + expect(queryAllByText('bonjour')).toHaveLength(0); }); }); diff --git a/packages/ra-ui-materialui/src/field/SelectField.tsx b/packages/ra-ui-materialui/src/field/SelectField.tsx index 229711798ab..e6ab140e66b 100644 --- a/packages/ra-ui-materialui/src/field/SelectField.tsx +++ b/packages/ra-ui-materialui/src/field/SelectField.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import get from 'lodash/get'; import pure from 'recompose/pure'; import compose from 'recompose/compose'; -import { withTranslate, TranslationContextProps } from 'ra-core'; +import { useTranslate } from 'ra-core'; import Typography from '@material-ui/core/Typography'; import sanitizeRestProps from './sanitizeRestProps'; @@ -83,19 +83,17 @@ interface Props extends FieldProps { * * **Tip**: <ReferenceField> sets `translateChoice` to false by default. */ -export const SelectField: SFC< - Props & InjectedFieldProps & TranslationContextProps -> = ({ +export const SelectField: SFC<Props & InjectedFieldProps> = ({ className, source, record, choices, optionValue, optionText, - translate, translateChoice, ...rest }) => { + const translate = useTranslate(); const value = get(record, source); const choice = choices.find(c => c[optionValue] === value); if (!choice) { @@ -126,15 +124,7 @@ SelectField.defaultProps = { translateChoice: true, }; -const enhance = compose< - Props & InjectedFieldProps & TranslationContextProps, - Props & TranslationContextProps ->( - pure, - withTranslate -); - -const EnhancedSelectField = enhance(SelectField); +const EnhancedSelectField = pure(SelectField); EnhancedSelectField.defaultProps = { addLabel: true, diff --git a/packages/ra-ui-materialui/src/input/ArrayInput.spec.js b/packages/ra-ui-materialui/src/input/ArrayInput.spec.js index e9a659f3bda..c42fc64c66a 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput.spec.js +++ b/packages/ra-ui-materialui/src/input/ArrayInput.spec.js @@ -11,7 +11,7 @@ import SimpleFormIterator from '../form/SimpleFormIterator'; describe('<ArrayInput />', () => { it('should render a FieldArray', () => { const wrapper = shallow(<ArrayInputView source="arr" record={{}} />); - expect(wrapper.find('translate(pure(FieldTitle))').length).toBe(1); + expect(wrapper.find('pure(FieldTitle)').length).toBe(1); expect(wrapper.find('FieldArray').length).toBe(1); }); diff --git a/packages/ra-ui-materialui/src/input/SearchInput.js b/packages/ra-ui-materialui/src/input/SearchInput.js index 460b52c4973..0aaa2103687 100644 --- a/packages/ra-ui-materialui/src/input/SearchInput.js +++ b/packages/ra-ui-materialui/src/input/SearchInput.js @@ -1,10 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import compose from 'recompose/compose'; import SearchIcon from '@material-ui/icons/Search'; import InputAdornment from '@material-ui/core/InputAdornment'; import { withStyles, createStyles } from '@material-ui/core/styles'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; import TextInput from './TextInput'; @@ -14,30 +13,27 @@ const searchFilterStyles = createStyles({ }, }); -const SearchInput = ({ classes, translate, ...props }) => ( - <TextInput - label={false} - placeholder={translate('ra.action.search')} - InputProps={{ - endAdornment: ( - <InputAdornment position="end"> - <SearchIcon color="disabled" /> - </InputAdornment> - ), - }} - className={classes.input} - {...props} - /> -); +const SearchInput = ({ classes, ...props }) => { + const translate = useTranslate(); + return ( + <TextInput + label={false} + placeholder={translate('ra.action.search')} + InputProps={{ + endAdornment: ( + <InputAdornment position="end"> + <SearchIcon color="disabled" /> + </InputAdornment> + ), + }} + className={classes.input} + {...props} + /> + ); +}; SearchInput.propTypes = { classes: PropTypes.object, - translate: PropTypes.func, }; -const enhance = compose( - translate, - withStyles(searchFilterStyles) -); - -export default enhance(SearchInput); +export default withStyles(searchFilterStyles)(SearchInput); diff --git a/packages/ra-ui-materialui/src/layout/Confirm.js b/packages/ra-ui-materialui/src/layout/Confirm.js index 465b1ff7bac..b9bfbb7ec66 100644 --- a/packages/ra-ui-materialui/src/layout/Confirm.js +++ b/packages/ra-ui-materialui/src/layout/Confirm.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; @@ -11,8 +11,7 @@ import { fade } from '@material-ui/core/styles/colorManipulator'; import ActionCheck from '@material-ui/icons/CheckCircle'; import AlertError from '@material-ui/icons/ErrorOutline'; import classnames from 'classnames'; -import compose from 'recompose/compose'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; const styles = theme => createStyles({ @@ -52,71 +51,68 @@ const styles = theme => * onClose={() => { // do something }} * /> */ -class Confirm extends Component { - state = { loading: false }; +const Confirm = ({ + isOpen, + title, + content, + confirm, + cancel, + confirmColor, + onClose, + onConfirm, + classes, + translateOptions = {}, +}) => { + const [loading, setLoading] = useState(false); + const translate = useTranslate(); - handleConfirm = e => { - e.stopPropagation(); - this.setState({ loading: true }); - this.props.onConfirm(); - }; - - render() { - const { - isOpen, - title, - content, - confirm, - cancel, - confirmColor, - onClose, - classes, - translate, - translateOptions = {}, - } = this.props; - const { loading } = this.state; + const handleConfirm = useCallback( + e => { + e.stopPropagation(); + setLoading(true); + onConfirm(); + }, + [onConfirm] + ); - return ( - <Dialog - open={isOpen} - onClose={onClose} - aria-labelledby="alert-dialog-title" - > - <DialogTitle id="alert-dialog-title"> - {translate(title, { _: title, ...translateOptions })} - </DialogTitle> - <DialogContent> - <DialogContentText className={classes.contentText}> - {translate(content, { - _: content, - ...translateOptions, - })} - </DialogContentText> - </DialogContent> - <DialogActions> - <Button disabled={loading} onClick={onClose}> - <AlertError className={classes.iconPaddingStyle} /> - {translate(cancel, { _: cancel })} - </Button> - <Button - disabled={loading} - onClick={this.handleConfirm} - className={classnames('ra-confirm', { - [classes.confirmWarning]: - confirmColor === 'warning', - [classes.confirmPrimary]: - confirmColor === 'primary', - })} - autoFocus - > - <ActionCheck className={classes.iconPaddingStyle} /> - {translate(confirm, { _: confirm })} - </Button> - </DialogActions> - </Dialog> - ); - } -} + return ( + <Dialog + open={isOpen} + onClose={onClose} + aria-labelledby="alert-dialog-title" + > + <DialogTitle id="alert-dialog-title"> + {translate(title, { _: title, ...translateOptions })} + </DialogTitle> + <DialogContent> + <DialogContentText className={classes.contentText}> + {translate(content, { + _: content, + ...translateOptions, + })} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button disabled={loading} onClick={onClose}> + <AlertError className={classes.iconPaddingStyle} /> + {translate(cancel, { _: cancel })} + </Button> + <Button + disabled={loading} + onClick={handleConfirm} + className={classnames('ra-confirm', { + [classes.confirmWarning]: confirmColor === 'warning', + [classes.confirmPrimary]: confirmColor === 'primary', + })} + autoFocus + > + <ActionCheck className={classes.iconPaddingStyle} /> + {translate(confirm, { _: confirm })} + </Button> + </DialogActions> + </Dialog> + ); +}; Confirm.propTypes = { cancel: PropTypes.string.isRequired, @@ -128,7 +124,6 @@ Confirm.propTypes = { onClose: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired, title: PropTypes.string.isRequired, - translate: PropTypes.func.isRequired, }; Confirm.defaultProps = { @@ -139,7 +134,4 @@ Confirm.defaultProps = { isOpen: false, }; -export default compose( - withStyles(styles), - translate -)(Confirm); +export default withStyles(styles)(Confirm); diff --git a/packages/ra-ui-materialui/src/layout/DashboardMenuItem.js b/packages/ra-ui-materialui/src/layout/DashboardMenuItem.js index 87d0ab5c564..d26fa745f23 100644 --- a/packages/ra-ui-materialui/src/layout/DashboardMenuItem.js +++ b/packages/ra-ui-materialui/src/layout/DashboardMenuItem.js @@ -1,27 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; import DashboardIcon from '@material-ui/icons/Dashboard'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; import MenuItemLink from './MenuItemLink'; -const DashboardMenuItem = ({ className, locale, onClick, translate, ...props }) => ( - <MenuItemLink - onClick={onClick} - to="/" - primaryText={translate('ra.page.dashboard')} - leftIcon={<DashboardIcon />} - exact - {...props} - /> -); +const DashboardMenuItem = ({ className, locale, onClick, ...props }) => { + const translate = useTranslate(); + return ( + <MenuItemLink + onClick={onClick} + to="/" + primaryText={translate('ra.page.dashboard')} + leftIcon={<DashboardIcon />} + exact + {...props} + /> + ); +}; DashboardMenuItem.propTypes = { classes: PropTypes.object, className: PropTypes.string, locale: PropTypes.string, onClick: PropTypes.func, - translate: PropTypes.func.isRequired, }; -export default translate(DashboardMenuItem); +export default DashboardMenuItem; diff --git a/packages/ra-ui-materialui/src/layout/Error.js b/packages/ra-ui-materialui/src/layout/Error.js index 0460fe58250..8a8c7e27b8d 100644 --- a/packages/ra-ui-materialui/src/layout/Error.js +++ b/packages/ra-ui-materialui/src/layout/Error.js @@ -1,7 +1,6 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import compose from 'recompose/compose'; import Button from '@material-ui/core/Button'; import ExpansionPanel from '@material-ui/core/ExpansionPanel'; import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails'; @@ -12,95 +11,89 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import History from '@material-ui/icons/History'; import Title from './Title'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; -const styles = theme => createStyles({ - container: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - [theme.breakpoints.down('sm')]: { - padding: '1em', +const styles = theme => + createStyles({ + container: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + [theme.breakpoints.down('sm')]: { + padding: '1em', + }, + fontFamily: 'Roboto, sans-serif', + opacity: 0.5, }, - fontFamily: 'Roboto, sans-serif', - opacity: 0.5, - }, - title: { - display: 'flex', - alignItems: 'center', - }, - icon: { - width: '2em', - height: '2em', - marginRight: '0.5em', - }, - panel: { - marginTop: '1em', - }, - panelDetails: { - whiteSpace: 'pre-wrap', - }, - toolbar: { - marginTop: '2em', - }, -}); + title: { + display: 'flex', + alignItems: 'center', + }, + icon: { + width: '2em', + height: '2em', + marginRight: '0.5em', + }, + panel: { + marginTop: '1em', + }, + panelDetails: { + whiteSpace: 'pre-wrap', + }, + toolbar: { + marginTop: '2em', + }, + }); function goBack() { history.go(-1); } -const Error = ({ - error, - errorInfo, - classes, - className, - title, - translate, - ...rest -}) => ( - <Fragment> - <Title defaultTitle={title} /> - <div className={classnames(classes.container, className)} {...rest}> - <h1 className={classes.title} role="alert"> - <ErrorIcon className={classes.icon} /> - {translate('ra.page.error')} - </h1> - <div>{translate('ra.message.error')}</div> - {process.env.NODE_ENV !== 'production' && ( - <ExpansionPanel className={classes.panel}> - <ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}> - {translate('ra.message.details')} - </ExpansionPanelSummary> - <ExpansionPanelDetails className={classes.panelDetails}> - <div> - <h2>{translate(error.toString())}</h2> - {errorInfo.componentStack} - </div> - </ExpansionPanelDetails> - </ExpansionPanel> - )} - <div className={classes.toolbar}> - <Button variant="raised" icon={<History />} onClick={goBack}> - {translate('ra.action.back')} - </Button> +const Error = ({ error, errorInfo, classes, className, title, ...rest }) => { + const translate = useTranslate(); + return ( + <Fragment> + <Title defaultTitle={title} /> + <div className={classnames(classes.container, className)} {...rest}> + <h1 className={classes.title} role="alert"> + <ErrorIcon className={classes.icon} /> + {translate('ra.page.error')} + </h1> + <div>{translate('ra.message.error')}</div> + {process.env.NODE_ENV !== 'production' && ( + <ExpansionPanel className={classes.panel}> + <ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}> + {translate('ra.message.details')} + </ExpansionPanelSummary> + <ExpansionPanelDetails className={classes.panelDetails}> + <div> + <h2>{translate(error.toString())}</h2> + {errorInfo.componentStack} + </div> + </ExpansionPanelDetails> + </ExpansionPanel> + )} + <div className={classes.toolbar}> + <Button + variant="raised" + icon={<History />} + onClick={goBack} + > + {translate('ra.action.back')} + </Button> + </div> </div> - </div> - </Fragment> -); + </Fragment> + ); +}; Error.propTypes = { classes: PropTypes.object, className: PropTypes.string, error: PropTypes.object.isRequired, errorInfo: PropTypes.object, - translate: PropTypes.func.isRequired, title: PropTypes.string, }; -const enhance = compose( - withStyles(styles), - translate -); - -export default enhance(Error); +export default withStyles(styles)(Error); diff --git a/packages/ra-ui-materialui/src/layout/Loading.js b/packages/ra-ui-materialui/src/layout/Loading.js index 9280083836f..d2ccb2209d8 100644 --- a/packages/ra-ui-materialui/src/layout/Loading.js +++ b/packages/ra-ui-materialui/src/layout/Loading.js @@ -1,56 +1,57 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withStyles, createStyles } from '@material-ui/core/styles'; -import compose from 'recompose/compose'; import classnames from 'classnames'; import CircularProgress from '@material-ui/core/CircularProgress'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; -const styles = theme => createStyles({ - container: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - [theme.breakpoints.up('md')]: { - height: '100%', +const styles = theme => + createStyles({ + container: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + [theme.breakpoints.up('md')]: { + height: '100%', + }, + [theme.breakpoints.down('sm')]: { + height: '100vh', + marginTop: '-3em', + }, }, - [theme.breakpoints.down('sm')]: { - height: '100vh', - marginTop: '-3em', + icon: { + width: '9em', + height: '9em', }, - }, - icon: { - width: '9em', - height: '9em', - }, - message: { - textAlign: 'center', - fontFamily: 'Roboto, sans-serif', - opacity: 0.5, - margin: '0 1em', - }, -}); + message: { + textAlign: 'center', + fontFamily: 'Roboto, sans-serif', + opacity: 0.5, + margin: '0 1em', + }, + }); const Loading = ({ classes, className, - translate, loadingPrimary = 'ra.page.loading', loadingSecondary = 'ra.message.loading', -}) => ( - <div className={classnames(classes.container, className)}> - <div className={classes.message}> - <CircularProgress className={classes.icon} color="primary" /> - <h1>{translate(loadingPrimary)}</h1> - <div>{translate(loadingSecondary)}.</div> +}) => { + const translate = useTranslate(); + return ( + <div className={classnames(classes.container, className)}> + <div className={classes.message}> + <CircularProgress className={classes.icon} color="primary" /> + <h1>{translate(loadingPrimary)}</h1> + <div>{translate(loadingSecondary)}.</div> + </div> </div> - </div> -); + ); +}; Loading.propTypes = { classes: PropTypes.object, className: PropTypes.string, - translate: PropTypes.func.isRequired, loadingPrimary: PropTypes.string, loadingSecondary: PropTypes.string, }; @@ -60,9 +61,4 @@ Loading.defaultProps = { loadingSecondary: 'ra.message.loading', }; -const enhance = compose( - withStyles(styles), - translate -); - -export default enhance(Loading); +export default withStyles(styles)(Loading); diff --git a/packages/ra-ui-materialui/src/layout/Menu.js b/packages/ra-ui-materialui/src/layout/Menu.js index aa1e3c57ce9..bf1dab983ee 100644 --- a/packages/ra-ui-materialui/src/layout/Menu.js +++ b/packages/ra-ui-materialui/src/layout/Menu.js @@ -5,7 +5,7 @@ import inflection from 'inflection'; import compose from 'recompose/compose'; import { withStyles, createStyles } from '@material-ui/core/styles'; import classnames from 'classnames'; -import { getResources, translate } from 'ra-core'; +import { getResources, useTranslate } from 'ra-core'; import DefaultIcon from '@material-ui/icons/ViewList'; import DashboardMenuItem from './DashboardMenuItem'; @@ -41,29 +41,34 @@ const Menu = ({ open, pathname, resources, - translate, logout, ...rest -}) => ( - <div className={classnames(classes.main, className)} {...rest}> - {hasDashboard && <DashboardMenuItem onClick={onMenuClick} />} - {resources - .filter(r => r.hasList) - .map(resource => ( - <MenuItemLink - key={resource.name} - to={`/${resource.name}`} - primaryText={translatedResourceName(resource, translate)} - leftIcon={ - resource.icon ? <resource.icon /> : <DefaultIcon /> - } - onClick={onMenuClick} - dense={dense} - /> - ))} - <Responsive xsmall={logout} medium={null} /> - </div> -); +}) => { + const translate = useTranslate(); + return ( + <div className={classnames(classes.main, className)} {...rest}> + {hasDashboard && <DashboardMenuItem onClick={onMenuClick} />} + {resources + .filter(r => r.hasList) + .map(resource => ( + <MenuItemLink + key={resource.name} + to={`/${resource.name}`} + primaryText={translatedResourceName( + resource, + translate + )} + leftIcon={ + resource.icon ? <resource.icon /> : <DefaultIcon /> + } + onClick={onMenuClick} + dense={dense} + /> + ))} + <Responsive xsmall={logout} medium={null} /> + </div> + ); +}; Menu.propTypes = { classes: PropTypes.object, @@ -75,7 +80,6 @@ Menu.propTypes = { open: PropTypes.bool, pathname: PropTypes.string, resources: PropTypes.array.isRequired, - translate: PropTypes.func.isRequired, }; Menu.defaultProps = { @@ -89,7 +93,6 @@ const mapStateToProps = state => ({ }); const enhance = compose( - translate, connect( mapStateToProps, {}, // Avoid connect passing dispatch in props, diff --git a/packages/ra-ui-materialui/src/layout/NotFound.js b/packages/ra-ui-materialui/src/layout/NotFound.js index a101f6d37b9..138cfeacab8 100644 --- a/packages/ra-ui-materialui/src/layout/NotFound.js +++ b/packages/ra-ui-materialui/src/layout/NotFound.js @@ -4,71 +4,68 @@ import Button from '@material-ui/core/Button'; import { withStyles, createStyles } from '@material-ui/core/styles'; import HotTub from '@material-ui/icons/HotTub'; import History from '@material-ui/icons/History'; -import compose from 'recompose/compose'; import classnames from 'classnames'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; import Title from './Title'; -const styles = theme => createStyles({ - container: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - [theme.breakpoints.up('md')]: { - height: '100%', +const styles = theme => + createStyles({ + container: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + [theme.breakpoints.up('md')]: { + height: '100%', + }, + [theme.breakpoints.down('sm')]: { + height: '100vh', + marginTop: '-3em', + }, }, - [theme.breakpoints.down('sm')]: { - height: '100vh', - marginTop: '-3em', + icon: { + width: '9em', + height: '9em', }, - }, - icon: { - width: '9em', - height: '9em', - }, - message: { - textAlign: 'center', - fontFamily: 'Roboto, sans-serif', - opacity: 0.5, - margin: '0 1em', - }, - toolbar: { - textAlign: 'center', - marginTop: '2em', - }, -}); + message: { + textAlign: 'center', + fontFamily: 'Roboto, sans-serif', + opacity: 0.5, + margin: '0 1em', + }, + toolbar: { + textAlign: 'center', + marginTop: '2em', + }, + }); function goBack() { history.go(-1); } -const NotFound = ({ classes, className, translate, title, ...rest }) => ( - <div className={classnames(classes.container, className)} {...rest}> - <Title defaultTitle={title} /> - <div className={classes.message}> - <HotTub className={classes.icon} /> - <h1>{translate('ra.page.not_found')}</h1> - <div>{translate('ra.message.not_found')}.</div> - </div> - <div className={classes.toolbar}> - <Button variant="raised" icon={<History />} onClick={goBack}> - {translate('ra.action.back')} - </Button> +const NotFound = ({ classes, className, title, ...rest }) => { + const translate = useTranslate(); + return ( + <div className={classnames(classes.container, className)} {...rest}> + <Title defaultTitle={title} /> + <div className={classes.message}> + <HotTub className={classes.icon} /> + <h1>{translate('ra.page.not_found')}</h1> + <div>{translate('ra.message.not_found')}.</div> + </div> + <div className={classes.toolbar}> + <Button variant="raised" icon={<History />} onClick={goBack}> + {translate('ra.action.back')} + </Button> + </div> </div> - </div> -); + ); +}; NotFound.propTypes = { classes: PropTypes.object, className: PropTypes.string, title: PropTypes.string, - translate: PropTypes.func.isRequired, }; -const enhance = compose( - withStyles(styles), - translate -); - -export default enhance(NotFound); +export default withStyles(styles)(NotFound); diff --git a/packages/ra-ui-materialui/src/layout/Title.js b/packages/ra-ui-materialui/src/layout/Title.js index 3522a8f99e1..af527619006 100644 --- a/packages/ra-ui-materialui/src/layout/Title.js +++ b/packages/ra-ui-materialui/src/layout/Title.js @@ -1,17 +1,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import { translate, warning } from 'ra-core'; +import { useTranslate, warning } from 'ra-core'; -const Title = ({ - className, - defaultTitle, - locale, - record, - title, - translate, - ...rest -}) => { +const Title = ({ className, defaultTitle, locale, record, title, ...rest }) => { + const translate = useTranslate(); const container = document.getElementById('react-admin-title'); if (!container) return null; warning(!defaultTitle && !title, 'Missing title prop in <Title> element'); @@ -35,8 +28,7 @@ Title.propTypes = { className: PropTypes.string, locale: PropTypes.string, record: PropTypes.object, - translate: PropTypes.func.isRequired, title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), }; -export default translate(Title); +export default Title; diff --git a/packages/ra-ui-materialui/src/layout/TitleDeprecated.js b/packages/ra-ui-materialui/src/layout/TitleDeprecated.js index ece2db8fff1..0ac321ee8ca 100644 --- a/packages/ra-ui-materialui/src/layout/TitleDeprecated.js +++ b/packages/ra-ui-materialui/src/layout/TitleDeprecated.js @@ -1,19 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; /** * @deprecated Use Title instead */ -const Title = ({ - className, - defaultTitle, - record, - title, - translate, - locale, - ...rest -}) => { +const Title = ({ className, defaultTitle, record, title, ...rest }) => { + const translate = useTranslate(); if (!title) { return ( <span className={className} {...rest}> @@ -36,8 +29,7 @@ Title.propTypes = { className: PropTypes.string, locale: PropTypes.string, record: PropTypes.object, - translate: PropTypes.func.isRequired, title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), }; -export default translate(Title); +export default Title; diff --git a/packages/ra-ui-materialui/src/list/BulkActionsToolbar.js b/packages/ra-ui-materialui/src/list/BulkActionsToolbar.js index cb8948bd567..fbaf98c751e 100644 --- a/packages/ra-ui-materialui/src/list/BulkActionsToolbar.js +++ b/packages/ra-ui-materialui/src/list/BulkActionsToolbar.js @@ -1,51 +1,51 @@ import React, { Children, cloneElement } from 'react'; import PropTypes from 'prop-types'; -import compose from 'recompose/compose'; import Toolbar from '@material-ui/core/Toolbar'; import Typography from '@material-ui/core/Typography'; import { withStyles, createStyles } from '@material-ui/core/styles'; import { lighten } from '@material-ui/core/styles/colorManipulator'; -import { translate, sanitizeListRestProps } from 'ra-core'; +import { useTranslate, sanitizeListRestProps } from 'ra-core'; import CardActions from '../layout/CardActions'; -const styles = theme => createStyles({ - toolbar: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - zIndex: 3, - color: - theme.palette.type === 'light' - ? theme.palette.primary.main - : theme.palette.text.primary, - justifyContent: 'space-between', - backgroundColor: - theme.palette.type === 'light' - ? lighten(theme.palette.primary.light, 0.85) - : theme.palette.primary.dark, - minHeight: 64, - height: 64, - transition: `${theme.transitions.create( - 'height' - )}, ${theme.transitions.create('min-height')}`, - }, - toolbarCollapsed: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - zIndex: 3, - minHeight: 0, - height: 0, - overflowY: 'hidden', - transition: theme.transitions.create('all'), - }, - title: { - flex: '0 0 auto', - }, -}); +const styles = theme => + createStyles({ + toolbar: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 3, + color: + theme.palette.type === 'light' + ? theme.palette.primary.main + : theme.palette.text.primary, + justifyContent: 'space-between', + backgroundColor: + theme.palette.type === 'light' + ? lighten(theme.palette.primary.light, 0.85) + : theme.palette.primary.dark, + minHeight: 64, + height: 64, + transition: `${theme.transitions.create( + 'height' + )}, ${theme.transitions.create('min-height')}`, + }, + toolbarCollapsed: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 3, + minHeight: 0, + height: 0, + overflowY: 'hidden', + transition: theme.transitions.create('all'), + }, + title: { + flex: '0 0 auto', + }, + }); const BulkActionsToolbar = ({ classes, @@ -54,11 +54,12 @@ const BulkActionsToolbar = ({ label, resource, selectedIds, - translate, children, ...rest -}) => - selectedIds.length > 0 ? ( +}) => { + const translate = useTranslate(); + + return selectedIds.length > 0 ? ( <Toolbar data-test="bulk-actions-toolbar" className={classes.toolbar} @@ -86,6 +87,7 @@ const BulkActionsToolbar = ({ ) : ( <Toolbar className={classes.toolbarCollapsed} /> ); +}; BulkActionsToolbar.propTypes = { children: PropTypes.node, @@ -95,16 +97,10 @@ BulkActionsToolbar.propTypes = { label: PropTypes.string, resource: PropTypes.string, selectedIds: PropTypes.array, - translate: PropTypes.func.isRequired, }; BulkActionsToolbar.defaultProps = { label: 'ra.action.bulk_actions', }; -const enhance = compose( - translate, - withStyles(styles) -); - -export default enhance(BulkActionsToolbar); +export default withStyles(styles)(BulkActionsToolbar); diff --git a/packages/ra-ui-materialui/src/list/DatagridHeaderCell.js b/packages/ra-ui-materialui/src/list/DatagridHeaderCell.js index 47a76f7ffda..28e091b0c63 100644 --- a/packages/ra-ui-materialui/src/list/DatagridHeaderCell.js +++ b/packages/ra-ui-materialui/src/list/DatagridHeaderCell.js @@ -7,7 +7,7 @@ import TableCell from '@material-ui/core/TableCell'; import TableSortLabel from '@material-ui/core/TableSortLabel'; import Tooltip from '@material-ui/core/Tooltip'; import { withStyles, createStyles } from '@material-ui/core/styles'; -import { FieldTitle, translate } from 'ra-core'; +import { FieldTitle, useTranslate } from 'ra-core'; // remove the sort icons when not active const styles = createStyles({ @@ -29,53 +29,55 @@ export const DatagridHeaderCell = ({ updateSort, resource, isSorting, - translate, ...rest -}) => ( - <TableCell - className={classnames(className, field.props.headerClassName)} - numeric={field.props.textAlign === 'right'} - padding="none" - variant="head" - {...rest} - > - {field.props.sortable !== false && - (field.props.sortBy || field.props.source) ? ( - <Tooltip - title={translate('ra.action.sort')} - placement={ - field.props.textAlign === 'right' - ? 'bottom-end' - : 'bottom-start' - } - enterDelay={300} - > - <TableSortLabel - active={ - currentSort.field === - (field.props.sortBy || field.props.source) +}) => { + const translate = useTranslate(); + return ( + <TableCell + className={classnames(className, field.props.headerClassName)} + numeric={field.props.textAlign === 'right'} + padding="none" + variant="head" + {...rest} + > + {field.props.sortable !== false && + (field.props.sortBy || field.props.source) ? ( + <Tooltip + title={translate('ra.action.sort')} + placement={ + field.props.textAlign === 'right' + ? 'bottom-end' + : 'bottom-start' } - direction={currentSort.order === 'ASC' ? 'asc' : 'desc'} - data-sort={field.props.sortBy || field.props.source} - onClick={updateSort} - classes={classes} + enterDelay={300} > - <FieldTitle - label={field.props.label} - source={field.props.source} - resource={resource} - /> - </TableSortLabel> - </Tooltip> - ) : ( - <FieldTitle - label={field.props.label} - source={field.props.source} - resource={resource} - /> - )} - </TableCell> -); + <TableSortLabel + active={ + currentSort.field === + (field.props.sortBy || field.props.source) + } + direction={currentSort.order === 'ASC' ? 'asc' : 'desc'} + data-sort={field.props.sortBy || field.props.source} + onClick={updateSort} + classes={classes} + > + <FieldTitle + label={field.props.label} + source={field.props.source} + resource={resource} + /> + </TableSortLabel> + </Tooltip> + ) : ( + <FieldTitle + label={field.props.label} + source={field.props.source} + resource={resource} + /> + )} + </TableCell> + ); +}; DatagridHeaderCell.propTypes = { classes: PropTypes.object, @@ -88,7 +90,6 @@ DatagridHeaderCell.propTypes = { isSorting: PropTypes.bool, sortable: PropTypes.bool, resource: PropTypes.string, - translate: PropTypes.func.isRequired, updateSort: PropTypes.func.isRequired, }; @@ -99,7 +100,6 @@ const enhance = compose( (nextProps.isSorting && props.currentSort.order !== nextProps.currentSort.order) ), - translate, withStyles(styles) ); diff --git a/packages/ra-ui-materialui/src/list/DatagridHeaderCell.spec.js b/packages/ra-ui-materialui/src/list/DatagridHeaderCell.spec.js index c43f199c992..420d8d0e235 100644 --- a/packages/ra-ui-materialui/src/list/DatagridHeaderCell.spec.js +++ b/packages/ra-ui-materialui/src/list/DatagridHeaderCell.spec.js @@ -1,10 +1,12 @@ -import assert from 'assert'; +import expect from 'expect'; import React from 'react'; -import { shallow } from 'enzyme'; +import { render, cleanup } from 'react-testing-library'; import { DatagridHeaderCell } from './DatagridHeaderCell'; describe('<DatagridHeaderCell />', () => { + afterEach(cleanup); + describe('sorting on a column', () => { const Field = () => <div />; Field.defaultProps = { @@ -13,67 +15,91 @@ describe('<DatagridHeaderCell />', () => { }; it('should be enabled when field has a source', () => { - const wrapper = shallow( - <DatagridHeaderCell - currentSort={{}} - field={<Field source="title" />} - updateSort={() => true} - translate={() => ''} - /> + const { getByTitle } = render( + <table> + <tbody> + <tr> + <DatagridHeaderCell + currentSort={{}} + field={<Field source="title" />} + updateSort={() => true} + /> + </tr> + </tbody> + </table> ); - assert.equal(wrapper.find('WithStyles(TableSortLabel)').length, 1); + expect(getByTitle('ra.action.sort').dataset.sort).toBe('title'); }); it('should be enabled when field has a sortBy props', () => { - const wrapper = shallow( - <DatagridHeaderCell - currentSort={{}} - field={<Field sortBy="title" />} - updateSort={() => true} - translate={() => ''} - /> + const { getByTitle } = render( + <table> + <tbody> + <tr> + <DatagridHeaderCell + currentSort={{}} + field={<Field sortBy="title" />} + updateSort={() => true} + /> + </tr> + </tbody> + </table> ); - assert.equal(wrapper.find('WithStyles(TableSortLabel)').length, 1); + expect(getByTitle('ra.action.sort').dataset.sort).toBe('title'); }); it('should be disabled when field has no sortby and no source', () => { - const wrapper = shallow( - <DatagridHeaderCell - currentSort={{}} - field={<Field />} - updateSort={() => true} - translate={() => ''} - /> + const { queryAllByTitle } = render( + <table> + <tbody> + <tr> + <DatagridHeaderCell + currentSort={{}} + field={<Field />} + updateSort={() => true} + /> + </tr> + </tbody> + </table> ); - - assert.equal(wrapper.find('WithStyles(TableSortLabel)').length, 0); + expect(queryAllByTitle('ra.action.sort')).toHaveLength(0); }); it('should be disabled when sortable prop is explicitly set to false', () => { - const wrapper = shallow( - <DatagridHeaderCell - currentSort={{}} - field={<Field source="title" sortable={false} />} - updateSort={() => true} - translate={() => ''} - /> + const { queryAllByTitle } = render( + <table> + <tbody> + <tr> + <DatagridHeaderCell + currentSort={{}} + field={ + <Field source="title" sortable={false} /> + } + updateSort={() => true} + /> + </tr> + </tbody> + </table> ); - - assert.equal(wrapper.find('WithStyles(TableSortLabel)').length, 0); + expect(queryAllByTitle('ra.action.sort')).toHaveLength(0); }); it('should use cell className if specified', () => { - const wrapper = shallow( - <DatagridHeaderCell - currentSort={{}} - updateSort={() => true} - translate={() => ''} - field={<Field />} - className="blue" - /> + const { container } = render( + <table> + <tbody> + <tr> + <DatagridHeaderCell + currentSort={{}} + updateSort={() => true} + field={<Field />} + className="blue" + /> + </tr> + </tbody> + </table> ); - const col = wrapper.find('WithStyles(TableCell)'); - assert.deepEqual(col.at(0).prop('className'), 'blue'); + expect(container.querySelector('td').className).toContain('blue'); }); }); }); diff --git a/packages/ra-ui-materialui/src/list/FilterFormInput.js b/packages/ra-ui-materialui/src/list/FilterFormInput.js index 2c041f19e94..b04267a5575 100644 --- a/packages/ra-ui-materialui/src/list/FilterFormInput.js +++ b/packages/ra-ui-materialui/src/list/FilterFormInput.js @@ -4,53 +4,47 @@ import { Field } from 'redux-form'; import IconButton from '@material-ui/core/IconButton'; import ActionHide from '@material-ui/icons/HighlightOff'; import classnames from 'classnames'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; const emptyRecord = {}; const sanitizeRestProps = ({ alwaysOn, ...props }) => props; -const FilterFormInput = ({ - filterElement, - handleHide, - classes, - resource, - translate, - locale, -}) => ( - <div - data-source={filterElement.props.source} - className={classnames('filter-field', classes.body)} - > - {!filterElement.props.alwaysOn && ( - <IconButton - className="hide-filter" - onClick={handleHide} - data-key={filterElement.props.source} - title={translate('ra.action.remove_filter')} - > - <ActionHide /> - </IconButton> - )} - <Field - allowEmpty - {...sanitizeRestProps(filterElement.props)} - name={filterElement.props.source} - component={filterElement.type} - resource={resource} - record={emptyRecord} - /> - <div className={classes.spacer}> </div> - </div> -); +const FilterFormInput = ({ filterElement, handleHide, classes, resource }) => { + const translate = useTranslate(); + return ( + <div + data-source={filterElement.props.source} + className={classnames('filter-field', classes.body)} + > + {!filterElement.props.alwaysOn && ( + <IconButton + className="hide-filter" + onClick={handleHide} + data-key={filterElement.props.source} + title={translate('ra.action.remove_filter')} + > + <ActionHide /> + </IconButton> + )} + <Field + allowEmpty + {...sanitizeRestProps(filterElement.props)} + name={filterElement.props.source} + component={filterElement.type} + resource={resource} + record={emptyRecord} + /> + <div className={classes.spacer}> </div> + </div> + ); +}; FilterFormInput.propTypes = { filterElement: PropTypes.node, handleHide: PropTypes.func, classes: PropTypes.object, resource: PropTypes.string, - locale: PropTypes.string, - translate: PropTypes.func, }; -export default translate(FilterFormInput); +export default FilterFormInput; diff --git a/packages/ra-ui-materialui/src/list/PaginationLimit.js b/packages/ra-ui-materialui/src/list/PaginationLimit.js index 96cbef0e7c0..97405609519 100644 --- a/packages/ra-ui-materialui/src/list/PaginationLimit.js +++ b/packages/ra-ui-materialui/src/list/PaginationLimit.js @@ -1,26 +1,18 @@ import React from 'react'; -import PropTypes from 'prop-types'; import pure from 'recompose/pure'; import CardContent from '@material-ui/core/CardContent'; import Typography from '@material-ui/core/Typography'; -import compose from 'recompose/compose'; -import { translate } from 'ra-core'; +import { useTranslate } from 'ra-core'; -const PaginationLimit = ({ translate }) => ( - <CardContent> - <Typography variant="body1"> - {translate('ra.navigation.no_results')} - </Typography> - </CardContent> -); - -PaginationLimit.propTypes = { - translate: PropTypes.func.isRequired, +const PaginationLimit = () => { + const translate = useTranslate(); + return ( + <CardContent> + <Typography variant="body1"> + {translate('ra.navigation.no_results')} + </Typography> + </CardContent> + ); }; -const enhance = compose( - pure, - translate -); - -export default enhance(PaginationLimit); +export default pure(PaginationLimit);