Skip to content

Commit

Permalink
Merge pull request #2984 from marmelab/typescript-migration-mui
Browse files Browse the repository at this point in the history
Migrate ra-ui-materialui to TypeScript (Part 1: auth)
  • Loading branch information
fzaninotto committed Apr 17, 2019
2 parents 99960f8 + a891d9c commit 089bae3
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 186 deletions.
6 changes: 5 additions & 1 deletion packages/ra-core/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import defaultI18nProvider from './defaultI18nProvider';
import translate from './translate';
import TranslationProvider from './TranslationProvider';

export { defaultI18nProvider, translate, TranslationProvider };
// Alias to translate to avoid shadowed variable names error with tsling
const withTranslate = translate;

export { defaultI18nProvider, translate, withTranslate, TranslationProvider };
export const DEFAULT_LOCALE = 'en';

export * from './TranslationUtils';
export * from './TranslationContext';
1 change: 1 addition & 0 deletions packages/ra-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ export {
} from './reducer/admin/references/oneToMany';

export * from './sideEffect';
export * from './types';
1 change: 1 addition & 0 deletions packages/ra-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface ReduxState {
[relatedTo: string]: { ids: Identifier[]; total: number };
};
};
loading: number;
};
i18n: {
locale: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import classNames from 'classnames';
import { Link as RRLink } from 'react-router-dom';
import { withStyles, createStyles } from '@material-ui/core/styles';

const styles = theme => createStyles({
link: {
textDecoration: 'none',
color: theme.palette.primary.main,
},
});
const styles = theme =>
createStyles({
link: {
textDecoration: 'none',
color: theme.palette.primary.main,
},
});

/**
* @deprecated Use react-router-dom's Link instead
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import React, { Component } from 'react';
import React, {
Component,
ReactElement,
ComponentType,
HtmlHTMLAttributes,
} from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Card from '@material-ui/core/Card';
Expand All @@ -8,49 +13,48 @@ import {
createMuiTheme,
withStyles,
createStyles,
WithStyles,
Theme,
} from '@material-ui/core/styles';
import LockIcon from '@material-ui/icons/Lock';
import { StaticContext } from 'react-router';

import defaultTheme from '../defaultTheme';
import Notification from '../layout/Notification';
import DefaultLoginForm from './LoginForm';

const styles = 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],
},
});
interface Props {
backgroundImage?: string;
loginForm: ReactElement<any>;
theme: object;
staticContext: StaticContext;
}

const sanitizeRestProps = ({
array,
backgroundImage,
classes,
className,
location,
staticContext,
theme,
title,
...rest
}) => rest;
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],
},
});

/**
* A standalone login page, to serve as authentication gate to the admin
Expand All @@ -70,31 +74,31 @@ const sanitizeRestProps = ({
* </Admin>
* );
*/
class Login extends Component {
constructor(props) {
super(props);
this.theme = createMuiTheme(props.theme);
this.containerRef = React.createRef();
this.backgroundImageLoaded = false;
}
class Login extends Component<
Props & WithStyles<typeof styles> & HtmlHTMLAttributes<HTMLDivElement>
> {
static propTypes = {
backgroundImage: PropTypes.string,
loginForm: PropTypes.element,
theme: PropTypes.object,
};

static defaultProps = {
backgroundImage: 'https://source.unsplash.com/random/1600x900/daily',
theme: defaultTheme,
loginForm: <DefaultLoginForm />,
};

// Even though the React doc ensure the ref creation is done before the
// componentDidMount, it can happen that the ref is set to null until the
// next render.
// So, to handle this case the component will now try to load the image on
// the componentDidMount, but if the ref doesn't exist, it will try again
// on the following componentDidUpdate. The try will be done only once.
// @see https://reactjs.org/docs/refs-and-the-dom.html#adding-a-ref-to-a-dom-element
updateBackgroundImage = (lastTry = false) => {
theme = createMuiTheme(this.props.theme);
containerRef = React.createRef<HTMLDivElement>();
backgroundImageLoaded = false;

updateBackgroundImage = () => {
if (!this.backgroundImageLoaded && this.containerRef.current) {
const { backgroundImage } = this.props;
this.containerRef.current.style.backgroundImage = `url(${backgroundImage})`;
this.backgroundImageLoaded = true;
}

if (lastTry) {
this.backgroundImageLoaded = true;
}
};

// Load background image asynchronously to speed up time to interactive
Expand All @@ -114,18 +118,25 @@ class Login extends Component {

componentDidUpdate() {
if (!this.backgroundImageLoaded) {
this.lazyLoadBackgroundImage(true);
this.lazyLoadBackgroundImage();
}
}

render() {
const { classes, className, loginForm, ...rest } = this.props;
const {
backgroundImage,
classes,
className,
loginForm,
staticContext,
...rest
} = this.props;

return (
<MuiThemeProvider theme={this.theme}>
<div
className={classnames(classes.main, className)}
{...sanitizeRestProps(rest)}
{...rest}
ref={this.containerRef}
>
<Card className={classes.card}>
Expand All @@ -143,21 +154,6 @@ class Login extends Component {
}
}

Login.propTypes = {
authProvider: PropTypes.func,
backgroundImage: PropTypes.string,
classes: PropTypes.object,
className: PropTypes.string,
input: PropTypes.object,
loginForm: PropTypes.element,
meta: PropTypes.object,
previousRoute: PropTypes.string,
};

Login.defaultProps = {
backgroundImage: 'https://source.unsplash.com/random/1600x900/daily',
theme: defaultTheme,
loginForm: <DefaultLoginForm />,
};
const EnhancedLogin = withStyles(styles)(Login) as ComponentType<Props>;

export default withStyles(styles)(Login);
export default EnhancedLogin;
Original file line number Diff line number Diff line change
@@ -1,26 +1,48 @@
import React from 'react';
import React, { SFC } from 'react';
import PropTypes from 'prop-types';
import { Field, propTypes, reduxForm } from 'redux-form';
import { Field, reduxForm, InjectedFormProps } from 'redux-form';
import { connect } from 'react-redux';
import compose from 'recompose/compose';
import CardActions from '@material-ui/core/CardActions';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import CircularProgress from '@material-ui/core/CircularProgress';
import { withStyles, createStyles } from '@material-ui/core/styles';
import { translate, userLogin } from 'ra-core';
import { withStyles, createStyles, WithStyles } from '@material-ui/core/styles';
import {
withTranslate,
userLogin,
TranslationContextProps,
ReduxState,
} from 'ra-core';

const styles = () => createStyles({
form: {
padding: '0 1em 1em 1em',
},
input: {
marginTop: '1em',
},
button: {
width: '100%',
},
});
interface Props {
redirectTo?: string;
}

interface FormData {
username: string;
password: string;
}

interface EnhancedProps
extends TranslationContextProps,
InjectedFormProps<FormData>,
WithStyles<typeof styles> {
isLoading: boolean;
}

const styles = () =>
createStyles({
form: {
padding: '0 1em 1em 1em',
},
input: {
marginTop: '1em',
},
button: {
width: '100%',
},
});

// see http://redux-form.com/6.4.3/examples/material-ui/
const renderInput = ({
Expand All @@ -39,7 +61,12 @@ const renderInput = ({
const login = (auth, dispatch, { redirectTo }) =>
dispatch(userLogin(auth, redirectTo));

const LoginForm = ({ classes, isLoading, handleSubmit, translate }) => (
const LoginForm: SFC<Props & EnhancedProps> = ({
classes,
isLoading,
handleSubmit,
translate,
}) => (
<form onSubmit={handleSubmit(login)}>
<div className={classes.form}>
<div className={classes.input}>
Expand Down Expand Up @@ -77,30 +104,35 @@ const LoginForm = ({ classes, isLoading, handleSubmit, translate }) => (
</CardActions>
</form>
);
LoginForm.propTypes = {
...propTypes,
classes: PropTypes.object,
redirectTo: PropTypes.string,
};

const mapStateToProps = state => ({ isLoading: state.admin.loading > 0 });
const mapStateToProps = (state: ReduxState) => ({
isLoading: state.admin.loading > 0,
});

const enhance = compose(
const enhance = compose<Props & EnhancedProps, Props>(
withStyles(styles),
translate,
withTranslate,
connect(mapStateToProps),
reduxForm({
form: 'signIn',
validate: (values, props) => {
const errors = {};
validate: (values: FormData, props: TranslationContextProps) => {
const errors = { username: '', password: '' };
const { translate } = props;
if (!values.username)
if (!values.username) {
errors.username = translate('ra.validation.required');
if (!values.password)
}
if (!values.password) {
errors.password = translate('ra.validation.required');
}
return errors;
},
})
);

export default enhance(LoginForm);
const EnhancedLoginForm = enhance(LoginForm);

EnhancedLoginForm.propTypes = {
redirectTo: PropTypes.string,
};

export default EnhancedLoginForm;
Loading

0 comments on commit 089bae3

Please sign in to comment.