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 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 {
static propTypes = {
title: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
fClose: PropTypes.func.isRequired,
fOnSubmit: PropTypes.func.isRequired,
};
interface IProps {
title: string
text: string
fClose: VoidFunction
fOnSubmit: VoidFunction
}
render() {
export default class ConfirmDialog extends Component<IProps> {
public render() {
const {title, text, fClose, fOnSubmit} = this.props;
const submitAndClose = () => {
fOnSubmit();

View File

@ -1,7 +1,7 @@
import React, {Component} from 'react';
import {withStyles} from 'material-ui/styles';
import {WithStyles} from "material-ui";
import Paper from 'material-ui/Paper';
import PropTypes from 'prop-types';
import {withStyles} from 'material-ui/styles';
import * as React from 'react';
const styles = () => ({
paper: {
@ -9,14 +9,12 @@ const styles = () => ({
},
});
class Container extends Component {
static propTypes = {
classes: PropTypes.object.isRequired,
children: PropTypes.node,
style: PropTypes.object,
};
interface IProps {
style?: object,
}
render() {
class Container extends React.Component<IProps & WithStyles<'paper'>, {}> {
public render() {
const {classes, children, style} = this.props;
return (
<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';
// See also https://github.com/coderiety/react-list/blob/master/react-list.es6
class FixedReactList extends ReactList {
// 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 :(
ignoreNextCacheUpdate = false;
public ignoreNextCacheUpdate = false;
cacheSizes() {
public cacheSizes(): void {
if (this.ignoreNextCacheUpdate) {
this.ignoreNextCacheUpdate = false;
return;
}
// @ts-ignore accessing private member
super.cacheSizes();
}
clearCacheFromIndex(startIndex) {
public clearCacheFromIndex(startIndex: number): void {
this.ignoreNextCacheUpdate = true;
if (startIndex === 0) {
// @ts-ignore accessing private member
this.cache = {};
} else {
// @ts-ignore accessing private member
Object.keys(this.cache).filter((index) => index >= startIndex).forEach((index) => {
// @ts-ignore accessing private member
delete this.cache[index];
});
}
};
componentDidUpdate() {
public componentDidUpdate() {
// @ts-ignore accessing private member
const hasCacheForLastRenderedItem = Object.keys(this.cache).length && this.cache[this.getVisibleRange()[1]];
// @ts-ignore accessing private member
super.componentDidUpdate();
if (!hasCacheForLastRenderedItem) {
// 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 AppBar from 'material-ui/AppBar';
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 {Theme, WithStyles} from "material-ui";
import AccountCircle from 'material-ui-icons/AccountCircle';
import Chat from 'material-ui-icons/Chat';
import SupervisorAccount from 'material-ui-icons/SupervisorAccount';
import DevicesOther from 'material-ui-icons/DevicesOther';
import ExitToApp from 'material-ui-icons/ExitToApp';
import AccountCircle from 'material-ui-icons/AccountCircle';
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: {
zIndex: theme.zIndex.drawer + 1,
},
@ -33,38 +33,19 @@ const styles = (theme) => ({
},
});
class Header extends Component {
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,
};
type Styles = WithStyles<'link' | 'titleName' | 'title' | 'appBar'>
renderButtons(name, admin) {
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>
);
interface IProps {
loggedIn: boolean
name: string
admin: boolean
version: string
toggleTheme: VoidFunction
showSettings: VoidFunction
}
render() {
class Header extends Component<IProps & Styles> {
public render() {
const {classes, version, name, loggedIn, admin, toggleTheme} = this.props;
return (
@ -88,6 +69,26 @@ class Header extends Component {
</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 {CircularProgress} from 'material-ui/Progress';
import React, {Component} from 'react';
import DefaultPage from './DefaultPage';
class LoadingSpinner extends Component {
render() {
public render() {
return (
<DefaultPage title="" maxWidth={250} hideButton={true}>
<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 Typography from 'material-ui/Typography';
import IconButton from 'material-ui/IconButton';
import PropTypes from 'prop-types';
import Container from './Container';
import React, {Component} from 'react';
import TimeAgo from 'react-timeago';
import Delete from 'material-ui-icons/Delete';
import Container from './Container';
const styles = () => ({
header: {
@ -32,16 +32,18 @@ const styles = () => ({
},
});
class Message extends Component {
static propTypes = {
classes: PropTypes.object.isRequired,
title: PropTypes.string.isRequired,
image: PropTypes.string,
date: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
fDelete: PropTypes.func.isRequired,
};
render() {
type Style = WithStyles<'header' | 'headerTitle' | 'trash' | 'wrapperPadding' | 'messageContentWrapper' | 'image' | 'imageWrapper'>;
interface IProps {
title: string
image?: string
date: string
content: string
fDelete: VoidFunction
}
class Message extends Component<IProps & Style> {
public render() {
const {fDelete, classes, title, date, content, image} = this.props;
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 Drawer from 'material-ui/Drawer';
import {ListItem, ListItemText} from 'material-ui/List';
import {withStyles} from 'material-ui/styles';
import PropTypes from 'prop-types';
import AppStore from '../stores/AppStore';
import React, {Component} from 'react';
import {Link} from 'react-router-dom';
import AppStore from '../stores/AppStore';
const styles = (theme) => ({
const styles = (theme: Theme) => ({
drawerPaper: {
position: 'relative',
position: 'relative' as 'relative',
width: 250,
minHeight: '100%',
},
toolbar: theme.mixins.toolbar,
toolbar: theme.mixins.toolbar as any,
link: {
color: 'inherit',
textDecoration: 'none',
},
});
class Navigation extends Component {
static propTypes = {
classes: PropTypes.object.isRequired,
loggedIn: PropTypes.bool.isRequired,
};
type Styles = WithStyles<'drawerPaper' | 'toolbar' | 'link'>
constructor() {
super();
this.state = {apps: []};
interface IProps {
loggedIn: boolean
}
componentWillMount() {
interface IState {
apps: IApplication[]
}
class Navigation extends Component<IProps & Styles, IState> {
public state: IState = {apps: []};
public componentWillMount() {
AppStore.on('change', this.updateApps);
}
componentWillUnmount() {
public componentWillUnmount() {
AppStore.removeListener('change', this.updateApps);
}
updateApps = () => this.setState({apps: AppStore.get()});
render() {
public render() {
const {classes, loggedIn} = this.props;
const {apps} = this.state;
@ -49,7 +49,7 @@ class Navigation extends Component {
<ListItemText primary="you have no applications :("/>
</ListItem>);
const userApps = apps.length === 0 ? empty : apps.map(function(app) {
const userApps = apps.length === 0 ? empty : apps.map((app) => {
return (
<Link className={classes.link} to={'/messages/' + app.id} key={app.id}>
<ListItem button>
@ -82,6 +82,8 @@ class Navigation extends Component {
</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 Button from 'material-ui/Button';
import React, {Component} from 'react';
class ScrollUpButton extends Component {
render() {
public render() {
return (
<Button variant="fab" color="primary"
style={{position: 'fixed', bottom: '30px', right: '30px', zIndex: 100000}}
onClick={() => window.scrollTo(0, 0)}>
onClick={this.scrollUp}>
<KeyboardArrowUp/>
</Button>
);
}
private scrollUp = () => window.scrollTo(0, 0);
}
export default ScrollUpButton;

View File

@ -1,28 +1,22 @@
import React, {Component} from 'react';
import Button from 'material-ui/Button';
import Dialog, {DialogActions, DialogContent, DialogTitle} from 'material-ui/Dialog';
import TextField from 'material-ui/TextField';
import Tooltip from 'material-ui/Tooltip';
import Dialog, {DialogActions, DialogContent, DialogTitle} from 'material-ui/Dialog';
import PropTypes from 'prop-types';
import React, {ChangeEvent, Component} from 'react';
import * as UserAction from '../actions/UserAction';
export default class SettingsDialog extends Component {
static propTypes = {
fClose: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {pass: ''};
interface IState {
pass: string
}
handleChange(propertyName, event) {
const state = this.state;
state[propertyName] = event.target.value;
this.setState(state);
interface IProps {
fClose: VoidFunction
}
render() {
export default class SettingsDialog extends Component<IProps, IState> {
public state = {pass: ''};
public render() {
const {pass} = this.state;
const {fClose} = this.props;
const submitAndClose = () => {
@ -50,4 +44,10 @@ export default class SettingsDialog extends Component {
</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 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: '',
hasNext: false,
open: false,
openWhen: 0,
};
componentWillMount = () => SnackBarStore.on('change', this.onNewSnack);
componentWillUnmount = () => SnackBarStore.removeListener('change', this.onNewSnack);
onNewSnack = () => {
const {open, openWhen} = this.state;
if (!open) {
this.openNextSnack();
return;
public componentWillMount() {
SnackBarStore.on('change', this.onNewSnack);
}
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);
public componentWillUnmount() {
SnackBarStore.removeListener('change', this.onNewSnack);
}
};
openNextSnack = () => {
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() {
public render() {
const {open, current, hasNext} = this.state;
const duration = hasNext
? 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;

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 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 {
static propTypes = {
value: PropTypes.string.isRequired,
style: PropTypes.object,
};
constructor() {
super();
this.state = {visible: false};
interface IProps {
value: string
style?: object
}
toggleVisibility = () => this.setState({visible: !this.state.visible});
interface IState {
visible: boolean
}
render() {
class ToggleVisibility extends Component<IProps, IState> {
public state = {visible: false};
public render() {
const {value, style} = this.props;
const text = this.state.visible ? value : '•••••••••••••••';
return (
@ -32,6 +30,8 @@ class ToggleVisibility extends Component {
</div>
);
}
private toggleVisibility = () => this.setState({visible: !this.state.visible});
}