diff --git a/packages/ra-ui-materialui/src/auth/Login.tsx b/packages/ra-ui-materialui/src/auth/Login.tsx index 38e0b8b2f6d..8bb89beaa37 100644 --- a/packages/ra-ui-materialui/src/auth/Login.tsx +++ b/packages/ra-ui-materialui/src/auth/Login.tsx @@ -1,20 +1,15 @@ import React, { - Component, - ComponentType, HtmlHTMLAttributes, ReactNode, + useRef, + useEffect, + useMemo, } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import Card from '@material-ui/core/Card'; import Avatar from '@material-ui/core/Avatar'; -import { - createMuiTheme, - withStyles, - createStyles, - WithStyles, - Theme, -} from '@material-ui/core/styles'; +import { createMuiTheme, makeStyles, Theme } from '@material-ui/core/styles'; import { ThemeProvider } from '@material-ui/styles'; import LockIcon from '@material-ui/icons/Lock'; import { StaticContext } from 'react-router'; @@ -30,31 +25,30 @@ interface Props { theme: object; } -const styles = (theme: Theme) => - createStyles({ - main: { - display: 'flex', - flexDirection: 'column', - minHeight: '100vh', - height: '1px', - alignItems: 'center', - justifyContent: 'flex-start', - backgroundRepeat: 'no-repeat', - backgroundSize: 'cover', - }, - card: { - minWidth: 300, - marginTop: '6em', - }, - avatar: { - margin: '1em', - display: 'flex', - justifyContent: 'center', - }, - icon: { - backgroundColor: theme.palette.secondary[500], - }, - }); +const useStyles = makeStyles((theme: Theme) => ({ + main: { + display: 'flex', + flexDirection: 'column', + minHeight: '100vh', + height: '1px', + alignItems: 'center', + justifyContent: 'flex-start', + backgroundRepeat: 'no-repeat', + backgroundSize: 'cover', + }, + card: { + minWidth: 300, + marginTop: '6em', + }, + avatar: { + margin: '1em', + display: 'flex', + justifyContent: 'center', + }, + icon: { + backgroundColor: theme.palette.secondary[500], + }, +})); /** * A standalone login page, to serve as authentication gate to the admin @@ -74,86 +68,75 @@ const styles = (theme: Theme) => * * ); */ -class Login extends Component< - Props & WithStyles & HtmlHTMLAttributes -> { - theme = createMuiTheme(this.props.theme); - containerRef = React.createRef(); - backgroundImageLoaded = false; +const Login: React.FunctionComponent< + Props & HtmlHTMLAttributes +> = ({ + theme, + className, + children, + staticContext, + backgroundImage, + ...rest +}) => { + const containerRef = useRef(); + const styles = useStyles({}); + const muiTheme = useMemo(() => createMuiTheme(theme), [theme]); + let backgroundImageLoaded = false; - updateBackgroundImage = () => { - if (!this.backgroundImageLoaded && this.containerRef.current) { - const { backgroundImage } = this.props; - this.containerRef.current.style.backgroundImage = `url(${backgroundImage})`; - this.backgroundImageLoaded = true; + const updateBackgroundImage = () => { + if (!backgroundImageLoaded && containerRef.current) { + containerRef.current.style.backgroundImage = `url(${backgroundImage})`; + backgroundImageLoaded = true; } }; // Load background image asynchronously to speed up time to interactive - lazyLoadBackgroundImage() { - const { backgroundImage } = this.props; - + const lazyLoadBackgroundImage = () => { if (backgroundImage) { const img = new Image(); - img.onload = this.updateBackgroundImage; + img.onload = updateBackgroundImage; img.src = backgroundImage; } - } - - componentDidMount() { - this.lazyLoadBackgroundImage(); - } + }; - componentDidUpdate() { - if (!this.backgroundImageLoaded) { - this.lazyLoadBackgroundImage(); + useEffect(() => { + if (!backgroundImageLoaded) { + lazyLoadBackgroundImage(); } - } - - render() { - const { - backgroundImage, - classes, - className, - children, - staticContext, - ...rest - } = this.props; - - return ( - -
- -
- - - -
- {children} -
- -
-
- ); - } -} + }); -const EnhancedLogin = withStyles(styles)(Login) as ComponentType; + return ( + +
+ +
+ + + +
+ {children} +
+ +
+
+ ); +}; -EnhancedLogin.propTypes = { +Login.propTypes = { backgroundImage: PropTypes.string, children: PropTypes.node, theme: PropTypes.object, staticContext: PropTypes.object, }; -EnhancedLogin.defaultProps = { +Login.defaultProps = { backgroundImage: 'https://source.unsplash.com/random/1600x900/daily', theme: defaultTheme, children: , }; -export default EnhancedLogin; + +export default Login; diff --git a/packages/ra-ui-materialui/src/layout/Notification.js b/packages/ra-ui-materialui/src/layout/Notification.js deleted file mode 100644 index 43dd027e078..00000000000 --- a/packages/ra-ui-materialui/src/layout/Notification.js +++ /dev/null @@ -1,168 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import Snackbar from '@material-ui/core/Snackbar'; -import Button from '@material-ui/core/Button'; -import { withStyles, createStyles } from '@material-ui/core/styles'; -import compose from 'recompose/compose'; -import classnames from 'classnames'; - -import { - hideNotification, - getNotification, - translate, - undo, - complete, - undoableEventEmitter, -} from 'ra-core'; - -const styles = theme => - createStyles({ - confirm: { - backgroundColor: theme.palette.background.default, - }, - warning: { - backgroundColor: theme.palette.error.light, - }, - undo: { - color: theme.palette.primary.light, - }, - }); - -class Notification extends React.Component { - state = { - open: false, - }; - componentWillMount = () => { - this.setOpenState(this.props); - }; - componentWillReceiveProps = nextProps => { - this.setOpenState(nextProps); - }; - - setOpenState = ({ notification }) => { - this.setState({ - open: !!notification, - }); - }; - - handleRequestClose = () => { - this.setState({ - open: false, - }); - }; - - handleExited = () => { - const { notification, hideNotification, complete } = this.props; - if (notification && notification.undoable) { - complete(); - undoableEventEmitter.emit('end', { isUndo: false }); - } - hideNotification(); - }; - - handleUndo = () => { - const { undo } = this.props; - undo(); - undoableEventEmitter.emit('end', { isUndo: true }); - }; - - render() { - const { - undo, - complete, - classes, - className, - type, - translate, - notification, - autoHideDuration, - hideNotification, - ...rest - } = this.props; - const { - warning, - confirm, - undo: undoClass, // Rename classes.undo to undoClass in this scope to avoid name conflicts - ...snackbarClasses - } = classes; - return ( - - {translate('ra.action.undo')} - - ) : null - } - classes={snackbarClasses} - {...rest} - /> - ); - } -} - -Notification.propTypes = { - complete: PropTypes.func, - classes: PropTypes.object, - className: PropTypes.string, - notification: PropTypes.shape({ - message: PropTypes.string, - type: PropTypes.string, - autoHideDuration: PropTypes.number, - messageArgs: PropTypes.object, - }), - type: PropTypes.string, - hideNotification: PropTypes.func.isRequired, - autoHideDuration: PropTypes.number, - translate: PropTypes.func.isRequired, - undo: PropTypes.func, -}; - -Notification.defaultProps = { - type: 'info', - autoHideDuration: 4000, -}; - -const mapStateToProps = state => ({ - notification: getNotification(state), -}); - -export default compose( - withStyles(styles), - translate, - connect( - mapStateToProps, - { - complete, - hideNotification, - undo, - } - ) -)(Notification); diff --git a/packages/ra-ui-materialui/src/layout/Notification.tsx b/packages/ra-ui-materialui/src/layout/Notification.tsx new file mode 100644 index 00000000000..e775d99111d --- /dev/null +++ b/packages/ra-ui-materialui/src/layout/Notification.tsx @@ -0,0 +1,111 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { useSelector, useDispatch } from 'react-redux'; +import Snackbar, { SnackbarProps } from '@material-ui/core/Snackbar'; +import Button from '@material-ui/core/Button'; +import { makeStyles, Theme } from '@material-ui/core/styles'; +import classnames from 'classnames'; + +import { + hideNotification, + getNotification, + undo, + complete, + undoableEventEmitter, + useTranslate, +} from 'ra-core'; + +interface Props { + type?: string; +} + +const useStyles = makeStyles((theme: Theme) => ({ + confirm: { + backgroundColor: theme.palette.background.default, + }, + warning: { + backgroundColor: theme.palette.error.light, + }, + undo: { + color: theme.palette.primary.light, + }, +})); + +const Notification: React.FunctionComponent< + Props & Omit +> = ({ type, className, autoHideDuration, ...rest }) => { + const [open, setOpen] = useState(false); + const notification = useSelector(getNotification); + const dispatch = useDispatch(); + const translate = useTranslate(); + const styles = useStyles({}); + + useEffect(() => { + setOpen(!!notification); + }, [notification]); + + const handleRequestClose = useCallback(() => { + setOpen(false); + }, []); + + const handleExited = useCallback(() => { + if (notification && notification.undoable) { + dispatch(complete()); + undoableEventEmitter.emit('end', { isUndo: false }); + } + dispatch(hideNotification()); + }, [dispatch, notification]); + + const handleUndo = useCallback(() => { + dispatch(undo()); + undoableEventEmitter.emit('end', { isUndo: true }); + }, [dispatch]); + + return ( + + {translate('ra.action.undo')} + + ) : null + } + {...rest} + /> + ); +}; + +Notification.propTypes = { + type: PropTypes.string, +}; + +Notification.defaultProps = { + type: 'info', + autoHideDuration: 4000, +}; + +export default Notification;