Typescriptify components

This commit is contained in:
Jannis Mattheis 2018-04-19 21:08:05 +02:00 committed by Jannis Mattheis
parent 08ae6d42bc
commit 51df6abd81
13 changed files with 241 additions and 231 deletions

View File

@ -1,17 +1,16 @@
import React, {Component} from 'react';
import Button from 'material-ui/Button'; import Button from 'material-ui/Button';
import Dialog, {DialogActions, DialogContent, DialogContentText, DialogTitle} from 'material-ui/Dialog'; import Dialog, {DialogActions, DialogContent, DialogContentText, DialogTitle} from 'material-ui/Dialog';
import PropTypes from 'prop-types'; import React, {Component} from 'react';
export default class ConfirmDialog extends Component { interface IProps {
static propTypes = { title: string
title: PropTypes.string.isRequired, text: string
text: PropTypes.string.isRequired, fClose: VoidFunction
fClose: PropTypes.func.isRequired, fOnSubmit: VoidFunction
fOnSubmit: PropTypes.func.isRequired, }
};
render() { export default class ConfirmDialog extends Component<IProps> {
public render() {
const {title, text, fClose, fOnSubmit} = this.props; const {title, text, fClose, fOnSubmit} = this.props;
const submitAndClose = () => { const submitAndClose = () => {
fOnSubmit(); fOnSubmit();

View File

@ -1,7 +1,7 @@
import React, {Component} from 'react'; import {WithStyles} from "material-ui";
import {withStyles} from 'material-ui/styles';
import Paper from 'material-ui/Paper'; import Paper from 'material-ui/Paper';
import PropTypes from 'prop-types'; import {withStyles} from 'material-ui/styles';
import * as React from 'react';
const styles = () => ({ const styles = () => ({
paper: { paper: {
@ -9,14 +9,12 @@ const styles = () => ({
}, },
}); });
class Container extends Component { interface IProps {
static propTypes = { style?: object,
classes: PropTypes.object.isRequired, }
children: PropTypes.node,
style: PropTypes.object,
};
render() { class Container extends React.Component<IProps & WithStyles<'paper'>, {}> {
public render() {
const {classes, children, style} = this.props; const {classes, children, style} = this.props;
return ( return (
<Paper elevation={6} className={classes.paper} style={style}> <Paper elevation={6} className={classes.paper} style={style}>
@ -26,4 +24,4 @@ class Container extends Component {
} }
} }
export default withStyles(styles)(Container); export default withStyles(styles)<IProps>(Container);

View File

@ -1,44 +0,0 @@
import React, {Component} from 'react';
import Button from 'material-ui/Button';
import Grid from 'material-ui/Grid';
import Typography from 'material-ui/Typography';
import PropTypes from 'prop-types';
export default class DefaultPage extends Component {
static defaultProps = {
buttonDisabled: false,
hideButton: false,
maxWidth: 700,
};
static propTypes = {
title: PropTypes.string.isRequired,
buttonTitle: PropTypes.string,
fButton: PropTypes.func,
buttonDisabled: PropTypes.bool.isRequired,
maxWidth: PropTypes.number.isRequired,
hideButton: PropTypes.bool.isRequired,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
render() {
const {title, buttonTitle, fButton, buttonDisabled, maxWidth, hideButton, children} = this.props;
return (
<main style={{margin: '0 auto', maxWidth}}>
<Grid container spacing={24}>
<Grid item xs={12} style={{display: 'flex'}}>
<Typography variant="display1" style={{flex: 1}}>
{title}
</Typography>
{hideButton ? null : <Button variant="raised" color="primary" disabled={buttonDisabled}
onClick={fButton}>{buttonTitle}</Button>}
</Grid>
{children}
</Grid>
</main>
);
}
}

View File

@ -0,0 +1,29 @@
import Button from 'material-ui/Button';
import Grid from 'material-ui/Grid';
import Typography from 'material-ui/Typography';
import React, {SFC} from 'react';
interface IProps {
title: string
buttonTitle?: string
fButton?: VoidFunction
buttonDisabled?: boolean
maxWidth?: number
hideButton?: boolean
}
const DefaultPage: SFC<IProps> = ({title, buttonTitle, fButton, buttonDisabled = false, maxWidth = 700, hideButton, children}) => (
<main style={{margin: '0 auto', maxWidth}}>
<Grid container spacing={24}>
<Grid item xs={12} style={{display: 'flex'}}>
<Typography variant="display1" style={{flex: 1}}>
{title}
</Typography>
{hideButton ? null : <Button variant="raised" color="primary" disabled={buttonDisabled}
onClick={fButton}>{buttonTitle}</Button>}
</Grid>
{children}
</Grid>
</main>
);
export default DefaultPage;

View File

@ -1,33 +1,41 @@
// @ts-ignore
import ReactList from 'react-list'; import ReactList from 'react-list';
// See also https://github.com/coderiety/react-list/blob/master/react-list.es6 // See also https://github.com/coderiety/react-list/blob/master/react-list.es6
class FixedReactList extends ReactList { class FixedReactList extends ReactList {
// deleting a messages or adding a message (per shift) requires invalidating the cache, react-list sucks as it does // deleting a messages or adding a message (per shift) requires invalidating the cache, react-list sucks as it does
// not provide such functionality, therefore we need to hack it inside there :( // not provide such functionality, therefore we need to hack it inside there :(
ignoreNextCacheUpdate = false; public ignoreNextCacheUpdate = false;
cacheSizes() { public cacheSizes(): void {
if (this.ignoreNextCacheUpdate) { if (this.ignoreNextCacheUpdate) {
this.ignoreNextCacheUpdate = false; this.ignoreNextCacheUpdate = false;
return; return;
} }
// @ts-ignore accessing private member
super.cacheSizes(); super.cacheSizes();
} }
clearCacheFromIndex(startIndex) {
public clearCacheFromIndex(startIndex: number): void {
this.ignoreNextCacheUpdate = true; this.ignoreNextCacheUpdate = true;
if (startIndex === 0) { if (startIndex === 0) {
// @ts-ignore accessing private member
this.cache = {}; this.cache = {};
} else { } else {
// @ts-ignore accessing private member
Object.keys(this.cache).filter((index) => index >= startIndex).forEach((index) => { Object.keys(this.cache).filter((index) => index >= startIndex).forEach((index) => {
// @ts-ignore accessing private member
delete this.cache[index]; delete this.cache[index];
}); });
} }
}; };
componentDidUpdate() { public componentDidUpdate() {
// @ts-ignore accessing private member
const hasCacheForLastRenderedItem = Object.keys(this.cache).length && this.cache[this.getVisibleRange()[1]]; const hasCacheForLastRenderedItem = Object.keys(this.cache).length && this.cache[this.getVisibleRange()[1]];
// @ts-ignore accessing private member
super.componentDidUpdate(); super.componentDidUpdate();
if (!hasCacheForLastRenderedItem) { if (!hasCacheForLastRenderedItem) {
// when there is no cache for the last rendered item, then its a new item, react-list doesn't know it size // when there is no cache for the last rendered item, then its a new item, react-list doesn't know it size

View File

@ -1,21 +1,21 @@
import React, {Component} from 'react'; import {Theme, WithStyles} from "material-ui";
import AppBar from 'material-ui/AppBar'; import AccountCircle from 'material-ui-icons/AccountCircle';
import Button from 'material-ui/Button';
import Toolbar from 'material-ui/Toolbar';
import Typography from 'material-ui/Typography';
import {withStyles} from 'material-ui/styles';
import PropTypes from 'prop-types';
import IconButton from 'material-ui/IconButton';
import * as UserAction from '../actions/UserAction';
import {Link} from 'react-router-dom';
import Chat from 'material-ui-icons/Chat'; import Chat from 'material-ui-icons/Chat';
import SupervisorAccount from 'material-ui-icons/SupervisorAccount';
import DevicesOther from 'material-ui-icons/DevicesOther'; import DevicesOther from 'material-ui-icons/DevicesOther';
import ExitToApp from 'material-ui-icons/ExitToApp'; import ExitToApp from 'material-ui-icons/ExitToApp';
import AccountCircle from 'material-ui-icons/AccountCircle';
import LightbulbOutline from 'material-ui-icons/LightbulbOutline'; import LightbulbOutline from 'material-ui-icons/LightbulbOutline';
import SupervisorAccount from 'material-ui-icons/SupervisorAccount';
import AppBar from 'material-ui/AppBar';
import Button from 'material-ui/Button';
import IconButton from 'material-ui/IconButton';
import {withStyles} from 'material-ui/styles';
import Toolbar from 'material-ui/Toolbar';
import Typography from 'material-ui/Typography';
import React, {Component} from 'react';
import {Link} from 'react-router-dom';
import * as UserAction from '../actions/UserAction';
const styles = (theme) => ({ const styles = (theme: Theme) => ({
appBar: { appBar: {
zIndex: theme.zIndex.drawer + 1, zIndex: theme.zIndex.drawer + 1,
}, },
@ -33,38 +33,19 @@ const styles = (theme) => ({
}, },
}); });
class Header extends Component { type Styles = WithStyles<'link' | 'titleName' | 'title' | 'appBar'>
static propTypes = {
classes: PropTypes.object.isRequired,
loggedIn: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
admin: PropTypes.bool.isRequired,
version: PropTypes.string.isRequired,
toggleTheme: PropTypes.func.isRequired,
showSettings: PropTypes.func.isRequired,
};
renderButtons(name, admin) { interface IProps {
const {classes, showSettings} = this.props; loggedIn: boolean
return ( name: string
<div> admin: boolean
{admin version: string
? <Link className={classes.link} to="/users"> toggleTheme: VoidFunction
<Button color="inherit"><SupervisorAccount/>&nbsp;users</Button></Link> showSettings: VoidFunction
: ''} }
<Link className={classes.link} to="/applications">
<Button color="inherit"><Chat/>&nbsp;apps</Button>
</Link>
<Link className={classes.link} to="/clients"><Button color="inherit">
<DevicesOther/>&nbsp;clients</Button>
</Link>
<Button color="inherit" onClick={showSettings}><AccountCircle/>&nbsp;{name}</Button>
<Button color="inherit" onClick={UserAction.logout}><ExitToApp/>&nbsp;Logout</Button>
</div>
);
}
render() { class Header extends Component<IProps & Styles> {
public render() {
const {classes, version, name, loggedIn, admin, toggleTheme} = this.props; const {classes, version, name, loggedIn, admin, toggleTheme} = this.props;
return ( return (
@ -88,6 +69,26 @@ class Header extends Component {
</AppBar> </AppBar>
); );
} }
private renderButtons(name: string, admin: boolean) {
const {classes, showSettings} = this.props;
return (
<div>
{admin
? <Link className={classes.link} to="/users">
<Button color="inherit"><SupervisorAccount/>&nbsp;users</Button></Link>
: ''}
<Link className={classes.link} to="/applications">
<Button color="inherit"><Chat/>&nbsp;apps</Button>
</Link>
<Link className={classes.link} to="/clients"><Button color="inherit">
<DevicesOther/>&nbsp;clients</Button>
</Link>
<Button color="inherit" onClick={showSettings}><AccountCircle/>&nbsp;{name}</Button>
<Button color="inherit" onClick={UserAction.logout}><ExitToApp/>&nbsp;Logout</Button>
</div>
);
}
} }
export default withStyles(styles, {withTheme: true})(Header); export default withStyles(styles, {withTheme: true})<IProps>(Header);

View File

@ -1,10 +1,10 @@
import React, {Component} from 'react';
import {CircularProgress} from 'material-ui/Progress';
import DefaultPage from './DefaultPage';
import Grid from 'material-ui/Grid'; import Grid from 'material-ui/Grid';
import {CircularProgress} from 'material-ui/Progress';
import React, {Component} from 'react';
import DefaultPage from './DefaultPage';
class LoadingSpinner extends Component { class LoadingSpinner extends Component {
render() { public render() {
return ( return (
<DefaultPage title="" maxWidth={250} hideButton={true}> <DefaultPage title="" maxWidth={250} hideButton={true}>
<Grid item xs={12} style={{textAlign: 'center'}}> <Grid item xs={12} style={{textAlign: 'center'}}>

View File

@ -1,11 +1,11 @@
import React, {Component} from 'react'; import {WithStyles} from "material-ui";
import Delete from 'material-ui-icons/Delete';
import IconButton from 'material-ui/IconButton';
import {withStyles} from 'material-ui/styles'; import {withStyles} from 'material-ui/styles';
import Typography from 'material-ui/Typography'; import Typography from 'material-ui/Typography';
import IconButton from 'material-ui/IconButton'; import React, {Component} from 'react';
import PropTypes from 'prop-types';
import Container from './Container';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import Delete from 'material-ui-icons/Delete'; import Container from './Container';
const styles = () => ({ const styles = () => ({
header: { header: {
@ -32,16 +32,18 @@ const styles = () => ({
}, },
}); });
class Message extends Component { type Style = WithStyles<'header' | 'headerTitle' | 'trash' | 'wrapperPadding' | 'messageContentWrapper' | 'image' | 'imageWrapper'>;
static propTypes = {
classes: PropTypes.object.isRequired, interface IProps {
title: PropTypes.string.isRequired, title: string
image: PropTypes.string, image?: string
date: PropTypes.string.isRequired, date: string
content: PropTypes.string.isRequired, content: string
fDelete: PropTypes.func.isRequired, fDelete: VoidFunction
}; }
render() {
class Message extends Component<IProps & Style> {
public render() {
const {fDelete, classes, title, date, content, image} = this.props; const {fDelete, classes, title, date, content, image} = this.props;
return ( return (
@ -68,4 +70,4 @@ class Message extends Component {
} }
} }
export default withStyles(styles)(Message); export default withStyles(styles)<IProps>(Message);

View File

@ -1,47 +1,47 @@
import React, {Component} from 'react'; import {Theme, WithStyles} from "material-ui";
import Divider from 'material-ui/Divider'; import Divider from 'material-ui/Divider';
import Drawer from 'material-ui/Drawer'; import Drawer from 'material-ui/Drawer';
import {ListItem, ListItemText} from 'material-ui/List'; import {ListItem, ListItemText} from 'material-ui/List';
import {withStyles} from 'material-ui/styles'; import {withStyles} from 'material-ui/styles';
import PropTypes from 'prop-types'; import React, {Component} from 'react';
import AppStore from '../stores/AppStore';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import AppStore from '../stores/AppStore';
const styles = (theme) => ({ const styles = (theme: Theme) => ({
drawerPaper: { drawerPaper: {
position: 'relative', position: 'relative' as 'relative',
width: 250, width: 250,
minHeight: '100%', minHeight: '100%',
}, },
toolbar: theme.mixins.toolbar, toolbar: theme.mixins.toolbar as any,
link: { link: {
color: 'inherit', color: 'inherit',
textDecoration: 'none', textDecoration: 'none',
}, },
}); });
class Navigation extends Component { type Styles = WithStyles<'drawerPaper' | 'toolbar' | 'link'>
static propTypes = {
classes: PropTypes.object.isRequired,
loggedIn: PropTypes.bool.isRequired,
};
constructor() { interface IProps {
super(); loggedIn: boolean
this.state = {apps: []}; }
}
componentWillMount() { interface IState {
apps: IApplication[]
}
class Navigation extends Component<IProps & Styles, IState> {
public state: IState = {apps: []};
public componentWillMount() {
AppStore.on('change', this.updateApps); AppStore.on('change', this.updateApps);
} }
componentWillUnmount() { public componentWillUnmount() {
AppStore.removeListener('change', this.updateApps); AppStore.removeListener('change', this.updateApps);
} }
updateApps = () => this.setState({apps: AppStore.get()}); public render() {
render() {
const {classes, loggedIn} = this.props; const {classes, loggedIn} = this.props;
const {apps} = this.state; const {apps} = this.state;
@ -49,7 +49,7 @@ class Navigation extends Component {
<ListItemText primary="you have no applications :("/> <ListItemText primary="you have no applications :("/>
</ListItem>); </ListItem>);
const userApps = apps.length === 0 ? empty : apps.map(function(app) { const userApps = apps.length === 0 ? empty : apps.map((app) => {
return ( return (
<Link className={classes.link} to={'/messages/' + app.id} key={app.id}> <Link className={classes.link} to={'/messages/' + app.id} key={app.id}>
<ListItem button> <ListItem button>
@ -82,6 +82,8 @@ class Navigation extends Component {
</Drawer> </Drawer>
); );
} }
private updateApps = () => this.setState({apps: AppStore.get()});
} }
export default withStyles(styles, {withTheme: true})(Navigation); export default withStyles(styles,{withTheme: true})<IProps>(Navigation);

View File

@ -1,17 +1,19 @@
import React, {Component} from 'react';
import Button from 'material-ui/Button';
import KeyboardArrowUp from 'material-ui-icons/KeyboardArrowUp'; import KeyboardArrowUp from 'material-ui-icons/KeyboardArrowUp';
import Button from 'material-ui/Button';
import React, {Component} from 'react';
class ScrollUpButton extends Component { class ScrollUpButton extends Component {
render() { public render() {
return ( return (
<Button variant="fab" color="primary" <Button variant="fab" color="primary"
style={{position: 'fixed', bottom: '30px', right: '30px', zIndex: 100000}} style={{position: 'fixed', bottom: '30px', right: '30px', zIndex: 100000}}
onClick={() => window.scrollTo(0, 0)}> onClick={this.scrollUp}>
<KeyboardArrowUp/> <KeyboardArrowUp/>
</Button> </Button>
); );
} }
private scrollUp = () => window.scrollTo(0, 0);
} }
export default ScrollUpButton; export default ScrollUpButton;

View File

@ -1,28 +1,22 @@
import React, {Component} from 'react';
import Button from 'material-ui/Button'; import Button from 'material-ui/Button';
import Dialog, {DialogActions, DialogContent, DialogTitle} from 'material-ui/Dialog';
import TextField from 'material-ui/TextField'; import TextField from 'material-ui/TextField';
import Tooltip from 'material-ui/Tooltip'; import Tooltip from 'material-ui/Tooltip';
import Dialog, {DialogActions, DialogContent, DialogTitle} from 'material-ui/Dialog'; import React, {ChangeEvent, Component} from 'react';
import PropTypes from 'prop-types';
import * as UserAction from '../actions/UserAction'; import * as UserAction from '../actions/UserAction';
export default class SettingsDialog extends Component { interface IState {
static propTypes = { pass: string
fClose: PropTypes.func.isRequired, }
};
constructor() { interface IProps {
super(); fClose: VoidFunction
this.state = {pass: ''}; }
}
handleChange(propertyName, event) { export default class SettingsDialog extends Component<IProps, IState> {
const state = this.state; public state = {pass: ''};
state[propertyName] = event.target.value;
this.setState(state);
}
render() { public render() {
const {pass} = this.state; const {pass} = this.state;
const {fClose} = this.props; const {fClose} = this.props;
const submitAndClose = () => { const submitAndClose = () => {
@ -50,4 +44,10 @@ export default class SettingsDialog extends Component {
</Dialog> </Dialog>
); );
} }
private handleChange(propertyName: string, event: ChangeEvent<HTMLInputElement>) {
const state = this.state;
state[propertyName] = event.target.value;
this.setState(state);
}
} }

View File

@ -1,54 +1,37 @@
import Close from 'material-ui-icons/Close';
import IconButton from 'material-ui/IconButton';
import Snackbar from 'material-ui/Snackbar';
import React, {Component} from 'react'; import React, {Component} from 'react';
import SnackBarStore from '../stores/SnackBarStore'; import SnackBarStore from '../stores/SnackBarStore';
import Snackbar from 'material-ui/Snackbar';
import IconButton from 'material-ui/IconButton';
import Close from 'material-ui-icons/Close';
class SnackBarHandler extends Component {
static MAX_VISIBLE_SNACK_TIME_IN_MS = 6000;
static MIN_VISIBLE_SNACK_TIME_IN_MS = 1000;
state = { interface IState {
current: string
hasNext: boolean
open: boolean
openWhen: number
}
class SnackBarHandler extends Component<{}, IState> {
private static MAX_VISIBLE_SNACK_TIME_IN_MS = 6000;
private static MIN_VISIBLE_SNACK_TIME_IN_MS = 1000;
public state = {
current: '', current: '',
hasNext: false, hasNext: false,
open: false, open: false,
openWhen: 0, openWhen: 0,
}; };
componentWillMount = () => SnackBarStore.on('change', this.onNewSnack); public componentWillMount() {
componentWillUnmount = () => SnackBarStore.removeListener('change', this.onNewSnack); SnackBarStore.on('change', this.onNewSnack);
onNewSnack = () => {
const {open, openWhen} = this.state;
if (!open) {
this.openNextSnack();
return;
} }
const snackOpenSince = Date.now() - openWhen; public componentWillUnmount() {
if (snackOpenSince > SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS) { SnackBarStore.removeListener('change', this.onNewSnack);
this.closeCurrentSnack();
} else {
setTimeout(this.closeCurrentSnack, SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS - snackOpenSince);
} }
};
openNextSnack = () => { public render() {
if (SnackBarStore.hasNext()) {
this.setState({
...this.state,
open: true,
openWhen: Date.now(),
current: SnackBarStore.next(),
hasNext: SnackBarStore.hasNext(),
});
}
};
closeCurrentSnack = () => this.setState({...this.state, open: false});
render() {
const {open, current, hasNext} = this.state; const {open, current, hasNext} = this.state;
const duration = hasNext const duration = hasNext
? SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS ? SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS
@ -68,6 +51,36 @@ class SnackBarHandler extends Component {
/> />
); );
} }
private onNewSnack = () => {
const {open, openWhen} = this.state;
if (!open) {
this.openNextSnack();
return;
}
const snackOpenSince = Date.now() - openWhen;
if (snackOpenSince > SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS) {
this.closeCurrentSnack();
} else {
setTimeout(this.closeCurrentSnack, SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS - snackOpenSince);
}
};
private openNextSnack = () => {
if (SnackBarStore.hasNext()) {
this.setState({
...this.state,
open: true,
openWhen: Date.now(),
current: SnackBarStore.next(),
hasNext: SnackBarStore.hasNext(),
});
}
};
private closeCurrentSnack = () => this.setState({...this.state, open: false});
} }
export default SnackBarHandler; export default SnackBarHandler;

View File

@ -1,24 +1,22 @@
import React, {Component} from 'react';
import Typography from 'material-ui/Typography';
import IconButton from 'material-ui/IconButton';
import VisibilityOff from 'material-ui-icons/VisibilityOff';
import Visibility from 'material-ui-icons/Visibility'; import Visibility from 'material-ui-icons/Visibility';
import PropTypes from 'prop-types'; import VisibilityOff from 'material-ui-icons/VisibilityOff';
import IconButton from 'material-ui/IconButton';
import Typography from 'material-ui/Typography';
import React, {Component} from 'react';
class ToggleVisibility extends Component { interface IProps {
static propTypes = { value: string
value: PropTypes.string.isRequired, style?: object
style: PropTypes.object, }
};
constructor() { interface IState {
super(); visible: boolean
this.state = {visible: false}; }
}
toggleVisibility = () => this.setState({visible: !this.state.visible}); class ToggleVisibility extends Component<IProps, IState> {
public state = {visible: false};
render() { public render() {
const {value, style} = this.props; const {value, style} = this.props;
const text = this.state.visible ? value : '•••••••••••••••'; const text = this.state.visible ? value : '•••••••••••••••';
return ( return (
@ -32,6 +30,8 @@ class ToggleVisibility extends Component {
</div> </div>
); );
} }
private toggleVisibility = () => this.setState({visible: !this.state.visible});
} }