implement notifications

This commit is contained in:
David Lechner
2020-04-20 13:57:58 -05:00
parent cf5d801a9a
commit b1e99e0582
12 changed files with 258 additions and 18 deletions
+7 -1
View File
@@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import { AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { flashFirmware } from '../actions/bootloader';
import * as notification from '../actions/notification';
import { RootState } from '../reducers';
import { BootloaderConnectionState } from '../reducers/bootloader';
import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton';
@@ -9,7 +10,7 @@ import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton';
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
type StateProps = Pick<OpenFileButtonProps, 'enabled'>;
type DispatchProps = Pick<OpenFileButtonProps, 'onFile'>;
type DispatchProps = Pick<OpenFileButtonProps, 'onFile' | 'onReject'>;
type OwnProps = Pick<OpenFileButtonProps, 'id'>;
const mapStateToProps = (state: RootState): StateProps => ({
@@ -20,6 +21,11 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onFile: (data): void => {
dispatch(flashFirmware(data));
},
onReject: (file): void => {
dispatch(
notification.add('error', `'${file.name}' is not a valid firmware file.`),
);
},
});
const mergeProps = (
+60
View File
@@ -0,0 +1,60 @@
import React from 'react';
import Toast from 'react-bootstrap/Toast';
import { connect } from 'react-redux';
import { Action } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import * as notification from '../actions/notification';
type Dispatch = ThunkDispatch<{}, {}, Action>;
interface DispatchProps {
onClose: () => void;
}
interface OwnProps {
id: number;
style: string;
message: string;
}
type NotificationProps = DispatchProps & OwnProps;
function mapTitle(style: string): string {
switch (style) {
case 'danger':
return 'Error';
case 'warning':
return 'Warning';
default:
return 'Info';
}
}
class Notification extends React.Component<NotificationProps> {
render(): JSX.Element {
const title = mapTitle(this.props.style);
return (
<Toast
onClose={(): void => {
this.props.onClose();
}}
transition={false}
>
<Toast.Header>
<strong className={`mr-auto text-${this.props.style}`}>
{title}
</strong>
</Toast.Header>
<Toast.Body>{this.props.message}</Toast.Body>
</Toast>
);
}
}
const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps): DispatchProps => ({
onClose: (): void => {
dispatch(notification.remove(ownProps.id));
},
});
export default connect(null, mapDispatchToProps)(Notification);
+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import { Collapse } from 'react-bootstrap';
import { connect } from 'react-redux';
import { TransitionGroup } from 'react-transition-group';
import { RootState } from '../reducers';
import { NotificationList } from '../reducers/notification';
import Notification from './Notification';
interface StateProps {
list: NotificationList;
}
type NotificationStackProps = StateProps;
class NotificationStack extends React.Component<NotificationStackProps> {
render(): JSX.Element {
return (
<div aria-live="polite" aria-atomic="true" style={{ position: 'relative' }}>
<div
style={{
position: 'absolute',
top: '10px',
right: '10px',
minWidth: '350px',
zIndex: 999,
}}
>
<TransitionGroup>
{this.props.list.map((n) => (
<Collapse key={n.id} in={true}>
<Notification
id={n.id}
style={n.style}
message={n.message}
/>
</Collapse>
))}
</TransitionGroup>
</div>
</div>
);
}
}
const mapStateToProps = (state: RootState): StateProps => ({
list: state.notification.list,
});
export default connect(mapStateToProps)(NotificationStack);
+4 -3
View File
@@ -16,8 +16,10 @@ export interface OpenFileButtonProps {
readonly icon: string;
/** When true or undefined, the button is enabled. */
readonly enabled?: boolean;
/** Callback that is called when the button is activated (clicked). */
/** Callback that is called when a file has been selected and opened for reading. */
readonly onFile: (data: ArrayBuffer) => void;
/** Callback that is called when a file has been rejected (e.g. bad file extension). */
readonly onReject: (file: File) => void;
}
/**
@@ -54,8 +56,7 @@ class OpenFileButton extends React.Component<OpenFileButtonProps> {
private onDropRejected(rejectedFiles: File[]): void {
// should only be one file since multiple={false}
rejectedFiles.forEach((f) => {
// TODO: proper bootstrap toast
alert(`bad file ${f.name}`);
this.props.onReject(f);
});
}