mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
implement notifications
This commit is contained in:
@@ -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 = (
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user