convert notifications to static API

This converts notifications from a reducer to a saga. This way, our
state doesn't get bogged down with a bunch of notifications.
This commit is contained in:
David Lechner
2021-01-18 17:29:03 -06:00
parent fc6ee4ff78
commit 54439a4cbc
11 changed files with 325 additions and 363 deletions
+36
View File
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { IToaster, Toaster } from '@blueprintjs/core';
import { I18nContext, I18nManager } from '@shopify/react-i18n';
import React from 'react';
import ReactDOM from 'react-dom';
/**
* Creates an `IToaster` for static usage similar to `Toaster.create()` except
* that it is wrapped in an `I18nContext.Provider` so that messages can be
* translated.
*
* @param i18n The i18n manager object.
*/
export function create(i18n: I18nManager): IToaster {
const containerElement = document.createElement('div');
document.body.appendChild(containerElement);
const toaster = React.createRef<Toaster>();
ReactDOM.render(
<I18nContext.Provider value={i18n}>
<Toaster usePortal={false} ref={toaster} />
</I18nContext.Provider>,
containerElement,
);
// istanbul ignore if: should not happen since we are rendering the component
if (toaster.current === null) {
throw new Error('failed to set toaster ref');
}
return toaster.current;
}
+12 -109
View File
@@ -1,121 +1,24 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { IconName, Intent, Toast } from '@blueprintjs/core';
import { Replacements, WithI18nProps, withI18n } from '@shopify/react-i18n';
import { Replacements, useI18n } from '@shopify/react-i18n';
import React from 'react';
import { connect } from 'react-redux';
import { Action, Dispatch } from '../actions';
import { NotificationLevel, remove } from '../actions/notification';
import { Level, MessageAction } from '../reducers/notification';
import { MessageId } from './notification-i18n';
import en from './notification-i18n.en.json';
interface DispatchProps {
onAction: (action: Action) => void;
onClose: () => void;
}
// provides translation for notification text
interface OwnProps {
id: number;
level: NotificationLevel;
message?: string;
messageId?: MessageId;
type OwnProps = {
messageId: MessageId;
replacements?: Replacements;
helpUrl?: string;
action?: MessageAction;
}
};
type NotificationProps = DispatchProps & OwnProps & WithI18nProps;
function mapIntent(level: NotificationLevel): Intent {
switch (level) {
case Level.Error:
return Intent.DANGER;
case Level.Warning:
return Intent.WARNING;
case Level.Info:
return Intent.PRIMARY;
default:
return Intent.NONE;
}
}
function mapIcon(level: NotificationLevel): IconName | undefined {
switch (level) {
case Level.Error:
return 'error';
case Level.Warning:
return 'warning-sign';
case Level.Info:
return 'info-sign';
default:
return undefined;
}
}
class Notification extends React.Component<NotificationProps> {
render(): JSX.Element {
const {
action,
helpUrl,
i18n,
level,
message,
messageId,
onAction,
onClose,
replacements,
} = this.props;
return (
<Toast
onDismiss={(): void => onClose()}
timeout={0}
intent={mapIntent(level)}
icon={mapIcon(level)}
message={
<div>
<p>
{messageId
? i18n.translate(messageId, replacements)
: message || 'missing message!'}
</p>
{helpUrl && (
<p>
<a
href={helpUrl}
target="_blank"
rel="noopener noreferrer"
>
More info
</a>
</p>
)}
</div>
}
action={
action && {
text: i18n.translate(action.titleId),
onClick: (): void => onAction(action.action),
}
}
/>
);
}
}
const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps): DispatchProps => ({
onAction: (a): Action => dispatch(a),
onClose: (): Action => dispatch(remove(ownProps.id)),
});
export default connect(
null,
mapDispatchToProps,
)(
withI18n({
export default function Notification(props: OwnProps): JSX.Element {
const [i18n] = useI18n({
id: 'notification',
fallback: en,
translations: { en },
})(Notification),
);
fallback: en,
});
const { messageId, replacements } = props;
return <>{i18n.translate(messageId, replacements)}</>;
}
-42
View File
@@ -1,42 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { Toaster } from '@blueprintjs/core';
import React from 'react';
import { connect } from 'react-redux';
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 (
<Toaster>
{this.props.list.map((n) => (
<Notification
id={n.id}
key={n.id}
level={n.level}
message={n.message}
messageId={n.messageId}
replacements={n.replacements}
helpUrl={n.helpUrl}
action={n.action}
/>
))}
</Toaster>
);
}
}
const mapStateToProps = (state: RootState): StateProps => ({
list: state.notification.list,
});
export default connect(mapStateToProps)(NotificationStack);
+4 -1
View File
@@ -7,7 +7,10 @@
},
"editor": {
"programChanged": "The program was changed in another window. Do you want to delete this program and replace it with the new program?",
"yesReloadProgram": "Yes"
"yesReloadProgram": "Reload"
},
"mpy": {
"error": "{errorMessage}"
},
"serviceWorker": {
"success": "Content is cached for offline use.",
+1
View File
@@ -12,4 +12,5 @@ export enum MessageId {
ServiceWorkerSuccess = 'serviceWorker.success',
ServiceWorkerUpdate = 'serviceWorker.update',
YesReloadProgram = 'editor.yesReloadProgram',
MpyError = 'mpy.error',
}