mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
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:
@@ -1,27 +1,16 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { Action } from 'redux';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
|
||||
export enum NotificationActionType {
|
||||
/**
|
||||
* Add a notification to the list of notifications.
|
||||
*/
|
||||
/** Add a notification to the list of notifications. */
|
||||
Add = 'notification.action.add',
|
||||
/**
|
||||
* Remove a notification from the list of notifications.
|
||||
*/
|
||||
Remove = 'notification.action.Remove',
|
||||
}
|
||||
|
||||
export type NotificationLevel = 'error' | 'warning' | 'info';
|
||||
|
||||
export type NotificationAddAction = Action<NotificationActionType.Add> & {
|
||||
/**
|
||||
* Unique ID for this notification instance.
|
||||
*/
|
||||
readonly id: number;
|
||||
/**
|
||||
* The type of notification.
|
||||
*/
|
||||
@@ -36,17 +25,6 @@ export type NotificationAddAction = Action<NotificationActionType.Add> & {
|
||||
readonly helpUrl?: string;
|
||||
};
|
||||
|
||||
export type NotificationRemoveAction = Action<NotificationActionType.Remove> & {
|
||||
/**
|
||||
* ID of an existing notification.
|
||||
*/
|
||||
readonly id: number;
|
||||
};
|
||||
|
||||
export type NotificationAction = NotificationAddAction | NotificationRemoveAction;
|
||||
|
||||
const nextId = createCountFunc();
|
||||
|
||||
/**
|
||||
* Action to add a notification to the list.
|
||||
* @param level The severity level
|
||||
@@ -58,13 +36,7 @@ export function add(
|
||||
message: string,
|
||||
helpUrl?: string,
|
||||
): NotificationAddAction {
|
||||
return { type: NotificationActionType.Add, id: -nextId(), level, message, helpUrl };
|
||||
return { type: NotificationActionType.Add, level, message, helpUrl };
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to removes a notification from the list.
|
||||
* @param id The id of the notification to remove
|
||||
*/
|
||||
export function remove(id: number): NotificationRemoveAction {
|
||||
return { type: NotificationActionType.Remove, id };
|
||||
}
|
||||
export type NotificationAction = NotificationAddAction;
|
||||
|
||||
@@ -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
@@ -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)}</>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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.",
|
||||
|
||||
@@ -12,4 +12,5 @@ export enum MessageId {
|
||||
ServiceWorkerSuccess = 'serviceWorker.success',
|
||||
ServiceWorkerUpdate = 'serviceWorker.update',
|
||||
YesReloadProgram = 'editor.yesReloadProgram',
|
||||
MpyError = 'mpy.error',
|
||||
}
|
||||
|
||||
+7
-6
@@ -12,21 +12,23 @@ import createSagaMiddleware from 'redux-saga';
|
||||
import './index.scss';
|
||||
import { didSucceed, didUpdate } from './actions/service-worker';
|
||||
import App from './components/App';
|
||||
import NotificationStack from './components/NotificationStack';
|
||||
import * as I18nToaster from './components/I18nToaster';
|
||||
import rootReducer from './reducers';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import rootSaga from './sagas';
|
||||
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
// TODO: add runtime option or filter - logger affects firmware flash performance
|
||||
const loggerMiddleware = createLogger({ predicate: () => false });
|
||||
|
||||
const i18n = new I18nManager({
|
||||
locale: 'en',
|
||||
onError: (err): void => console.error(err),
|
||||
});
|
||||
|
||||
const toaster = I18nToaster.create(i18n);
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware({ context: { notification: { toaster } } });
|
||||
// TODO: add runtime option or filter - logger affects firmware flash performance
|
||||
const loggerMiddleware = createLogger({ predicate: () => false });
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(sagaMiddleware, loggerMiddleware),
|
||||
@@ -69,7 +71,6 @@ ReactDOM.render(
|
||||
>
|
||||
<div id="vh" className="h-100 w-100 p-absolute" />
|
||||
</ResizeSensor>
|
||||
<NotificationStack />
|
||||
<App />
|
||||
</I18nContext.Provider>
|
||||
</Provider>
|
||||
|
||||
@@ -8,7 +8,6 @@ import bootloader, { BootloaderState } from './bootloader';
|
||||
import editor, { EditorState } from './editor';
|
||||
import hub, { HubState } from './hub';
|
||||
import license, { LicenseState } from './license';
|
||||
import notification, { NotificationState } from './notification';
|
||||
import settings, { SettingsState } from './settings';
|
||||
import status, { StatusState } from './status';
|
||||
import terminal, { TerminalState } from './terminal';
|
||||
@@ -23,7 +22,6 @@ export interface RootState {
|
||||
readonly editor: EditorState;
|
||||
readonly hub: HubState;
|
||||
readonly license: LicenseState;
|
||||
readonly notification: NotificationState;
|
||||
readonly settings: SettingsState;
|
||||
readonly status: StatusState;
|
||||
readonly terminal: TerminalState;
|
||||
@@ -36,7 +34,6 @@ export default combineReducers({
|
||||
editor,
|
||||
hub,
|
||||
license,
|
||||
notification,
|
||||
settings,
|
||||
status,
|
||||
terminal,
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { Replacements } from '@shopify/react-i18n';
|
||||
import { Reducer } from 'react';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Action } from '../actions';
|
||||
import { BleDeviceActionType, BleDeviceFailToConnectReasonType } from '../actions/ble';
|
||||
import { EditorActionType, reloadProgram } from '../actions/editor';
|
||||
import {
|
||||
BootloaderConnectionActionType,
|
||||
BootloaderConnectionFailureReason,
|
||||
} from '../actions/lwp3-bootloader';
|
||||
import { MpyActionType } from '../actions/mpy';
|
||||
import { NotificationActionType } from '../actions/notification';
|
||||
import { ServiceWorkerActionType } from '../actions/service-worker';
|
||||
import { MessageId } from '../components/notification-i18n';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
|
||||
/**
|
||||
* Severity level of notification.
|
||||
*/
|
||||
export enum Level {
|
||||
/**
|
||||
* This is an error (requires user action to resolve)
|
||||
*/
|
||||
Error = 'error',
|
||||
/**
|
||||
* This is a warning (user could take action or ignore)
|
||||
*/
|
||||
Warning = 'warning',
|
||||
/**
|
||||
* This is just FYI (no user action required)
|
||||
*/
|
||||
Info = 'info',
|
||||
}
|
||||
|
||||
export interface MessageAction {
|
||||
titleId: MessageId;
|
||||
action: Action;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
readonly id: number;
|
||||
readonly level: Level;
|
||||
readonly message?: string;
|
||||
readonly messageId?: MessageId;
|
||||
readonly replacements?: Replacements;
|
||||
readonly helpUrl?: string;
|
||||
readonly action?: MessageAction;
|
||||
}
|
||||
|
||||
export type NotificationList = Array<Notification>;
|
||||
|
||||
const nextId = createCountFunc();
|
||||
|
||||
function append(
|
||||
state: NotificationList,
|
||||
level: Level,
|
||||
messageId: MessageId,
|
||||
replacements?: Replacements,
|
||||
helpUrl?: string,
|
||||
action?: MessageAction,
|
||||
): NotificationList {
|
||||
return [
|
||||
...state,
|
||||
{ id: nextId(), level, messageId, replacements, helpUrl, action },
|
||||
];
|
||||
}
|
||||
|
||||
const list: Reducer<NotificationList, Action> = (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case BleDeviceActionType.DidFailToConnect:
|
||||
switch (action.reason) {
|
||||
case BleDeviceFailToConnectReasonType.NoGatt:
|
||||
return append(state, Level.Error, MessageId.BleGattPermission);
|
||||
case BleDeviceFailToConnectReasonType.NoService:
|
||||
return append(
|
||||
state,
|
||||
Level.Error,
|
||||
MessageId.BleGattServiceNotFound,
|
||||
{ serviceName: 'Pybricks', hubName: 'Pybricks Hub' },
|
||||
);
|
||||
case BleDeviceFailToConnectReasonType.NoWebBluetooth:
|
||||
return append(
|
||||
state,
|
||||
Level.Error,
|
||||
MessageId.BleNoWebBluetooth,
|
||||
undefined,
|
||||
'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
|
||||
);
|
||||
case BleDeviceFailToConnectReasonType.Unknown:
|
||||
return append(state, Level.Error, MessageId.BleConnectFailed);
|
||||
}
|
||||
return state;
|
||||
case BootloaderConnectionActionType.DidFailToConnect:
|
||||
switch (action.reason) {
|
||||
case BootloaderConnectionFailureReason.GattServiceNotFound:
|
||||
return append(
|
||||
state,
|
||||
Level.Error,
|
||||
MessageId.BleGattServiceNotFound,
|
||||
{ serviceName: 'LEGO Bootloader', hubName: 'LEGO Bootloader' },
|
||||
);
|
||||
case BootloaderConnectionFailureReason.NoWebBluetooth:
|
||||
return append(
|
||||
state,
|
||||
Level.Error,
|
||||
MessageId.BleNoWebBluetooth,
|
||||
undefined,
|
||||
'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
|
||||
);
|
||||
case BootloaderConnectionFailureReason.Unknown:
|
||||
return append(state, Level.Error, MessageId.BleConnectFailed);
|
||||
}
|
||||
return state;
|
||||
case EditorActionType.StorageChanged:
|
||||
if (state.find((x) => x.messageId === MessageId.ProgramChanged)) {
|
||||
// don't show message again if it is already shown
|
||||
return state;
|
||||
}
|
||||
return append(
|
||||
state,
|
||||
Level.Info,
|
||||
MessageId.ProgramChanged,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
titleId: MessageId.YesReloadProgram,
|
||||
action: reloadProgram(),
|
||||
},
|
||||
);
|
||||
case MpyActionType.DidFailToCompile:
|
||||
return [
|
||||
...state,
|
||||
{ id: nextId(), level: Level.Error, message: action.err },
|
||||
];
|
||||
case NotificationActionType.Add:
|
||||
return [
|
||||
...state,
|
||||
{
|
||||
id: action.id,
|
||||
level: action.level as Level,
|
||||
message: action.message,
|
||||
helpUrl: action.helpUrl,
|
||||
},
|
||||
];
|
||||
case NotificationActionType.Remove:
|
||||
return state.filter((e) => e.id !== action.id);
|
||||
case ServiceWorkerActionType.DidUpdate:
|
||||
return append(
|
||||
state,
|
||||
Level.Info,
|
||||
MessageId.ServiceWorkerUpdate,
|
||||
undefined,
|
||||
'https://github.com/pybricks/pybricks-code/issues/102',
|
||||
);
|
||||
case ServiceWorkerActionType.DidSucceed:
|
||||
return append(state, Level.Info, MessageId.ServiceWorkerSuccess);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export interface NotificationState {
|
||||
readonly list: NotificationList;
|
||||
}
|
||||
|
||||
export default combineReducers({ list });
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { all, put } from 'redux-saga/effects';
|
||||
import { startup } from '../actions/app';
|
||||
@@ -12,6 +12,7 @@ import license from './license';
|
||||
import lwp3BootloaderBle from './lwp3-bootloader-ble';
|
||||
import lwp3BootloaderProtocol from './lwp3-bootloader-protocol';
|
||||
import mpy from './mpy';
|
||||
import notification from './notification';
|
||||
import settings from './settings';
|
||||
import terminal from './terminal';
|
||||
|
||||
@@ -27,6 +28,7 @@ export default function* (): Generator {
|
||||
hub(),
|
||||
license(),
|
||||
mpy(),
|
||||
notification(),
|
||||
settings(),
|
||||
terminal(),
|
||||
put(startup()),
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
// Saga for managing notifications (toasts)
|
||||
|
||||
import {
|
||||
IActionProps,
|
||||
ILinkProps,
|
||||
IToaster,
|
||||
IconName,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import { Replacements } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { channel } from 'redux-saga';
|
||||
import { delay, getContext, put, take, takeEvery } from 'redux-saga/effects';
|
||||
import {
|
||||
BleDeviceActionType,
|
||||
BleDeviceDidFailToConnectAction,
|
||||
BleDeviceFailToConnectReasonType,
|
||||
} from '../actions/ble';
|
||||
import { EditorActionType, reloadProgram } from '../actions/editor';
|
||||
import {
|
||||
BootloaderConnectionActionType,
|
||||
BootloaderConnectionDidFailToConnectAction,
|
||||
BootloaderConnectionFailureReason,
|
||||
} from '../actions/lwp3-bootloader';
|
||||
import { MpyActionType, MpyDidFailToCompileAction } from '../actions/mpy';
|
||||
import { NotificationActionType, NotificationAddAction } from '../actions/notification';
|
||||
import { ServiceWorkerActionType } from '../actions/service-worker';
|
||||
import Notification from '../components/Notification';
|
||||
import { MessageId } from '../components/notification-i18n';
|
||||
|
||||
type NotificationContext = {
|
||||
toaster: IToaster;
|
||||
};
|
||||
|
||||
/** Severity level of notification. */
|
||||
enum Level {
|
||||
/** This is an error (requires user action to resolve). */
|
||||
Error = 'error',
|
||||
/** This is a warning (user could take action or ignore). */
|
||||
Warning = 'warning',
|
||||
/** This is just FYI (no user action required). */
|
||||
Info = 'info',
|
||||
}
|
||||
|
||||
function mapIntent(level: Level): 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: Level): IconName | undefined {
|
||||
switch (level) {
|
||||
case Level.Error:
|
||||
return 'error';
|
||||
case Level.Warning:
|
||||
return 'warning-sign';
|
||||
case Level.Info:
|
||||
return 'info-sign';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a URL to an action that can be passed to `IToaster.show()`.
|
||||
* @param helpUrl A URL.
|
||||
*/
|
||||
function helpAction(helpUrl: string): IActionProps & ILinkProps {
|
||||
return {
|
||||
icon: 'help',
|
||||
href: helpUrl,
|
||||
target: '_blank',
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchAction(
|
||||
messageId: MessageId,
|
||||
onClick: (event: React.MouseEvent<HTMLElement>) => void,
|
||||
icon?: IconName,
|
||||
): IActionProps {
|
||||
return {
|
||||
icon: icon,
|
||||
text: React.createElement(Notification, { messageId }),
|
||||
onClick,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message. If a message with the same `messageId` is already
|
||||
* showing, it will be closed before showing the new message.
|
||||
* @param level The severity level.
|
||||
* @param messageId The translation lookup ID.
|
||||
* @param replacements Replacements for the translation string.
|
||||
* @param action Optional action to add to the notification.
|
||||
* @param onDismiss Optional hook for when notification is dismissed.
|
||||
*/
|
||||
function* showSingleton(
|
||||
level: Level,
|
||||
messageId: MessageId,
|
||||
replacements?: Replacements,
|
||||
action?: IActionProps & ILinkProps,
|
||||
onDismiss?: (didTimeoutExpire: boolean) => void,
|
||||
): Generator {
|
||||
const { toaster } = (yield getContext('notification')) as NotificationContext;
|
||||
|
||||
// if the message is already showing, close it and wait some time so that
|
||||
// users can see that something triggered the message again
|
||||
if (
|
||||
toaster
|
||||
.getToasts()
|
||||
.map((x) => x.key)
|
||||
.includes(messageId)
|
||||
) {
|
||||
toaster.dismiss(messageId);
|
||||
yield delay(500);
|
||||
}
|
||||
|
||||
toaster.show(
|
||||
{
|
||||
intent: mapIntent(level),
|
||||
icon: mapIcon(level),
|
||||
message: React.createElement(Notification, { messageId, replacements }),
|
||||
timeout: 0,
|
||||
action,
|
||||
onDismiss,
|
||||
},
|
||||
messageId,
|
||||
);
|
||||
}
|
||||
|
||||
function* showBleDeviceDidFailToConnectError(
|
||||
action: BleDeviceDidFailToConnectAction,
|
||||
): Generator {
|
||||
switch (action.reason) {
|
||||
case BleDeviceFailToConnectReasonType.NoGatt:
|
||||
yield* showSingleton(Level.Error, MessageId.BleGattPermission);
|
||||
break;
|
||||
|
||||
case BleDeviceFailToConnectReasonType.NoService:
|
||||
yield* showSingleton(Level.Error, MessageId.BleGattServiceNotFound, {
|
||||
serviceName: 'Pybricks',
|
||||
hubName: 'Pybricks Hub',
|
||||
});
|
||||
break;
|
||||
case BleDeviceFailToConnectReasonType.NoWebBluetooth:
|
||||
yield* showSingleton(
|
||||
Level.Error,
|
||||
MessageId.BleNoWebBluetooth,
|
||||
undefined,
|
||||
helpAction(
|
||||
'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
|
||||
),
|
||||
);
|
||||
break;
|
||||
case BleDeviceFailToConnectReasonType.Unknown:
|
||||
yield* showSingleton(Level.Error, MessageId.BleConnectFailed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function* showBootloaderDidFailToConnectError(
|
||||
action: BootloaderConnectionDidFailToConnectAction,
|
||||
): Generator {
|
||||
switch (action.reason) {
|
||||
case BootloaderConnectionFailureReason.GattServiceNotFound:
|
||||
yield* showSingleton(Level.Error, MessageId.BleGattServiceNotFound, {
|
||||
serviceName: 'LEGO Bootloader',
|
||||
hubName: 'LEGO Bootloader',
|
||||
});
|
||||
break;
|
||||
case BootloaderConnectionFailureReason.NoWebBluetooth:
|
||||
yield* showSingleton(
|
||||
Level.Error,
|
||||
MessageId.BleNoWebBluetooth,
|
||||
undefined,
|
||||
helpAction(
|
||||
'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
|
||||
),
|
||||
);
|
||||
break;
|
||||
case BootloaderConnectionFailureReason.Unknown:
|
||||
yield* showSingleton(Level.Error, MessageId.BleConnectFailed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function* showEditorStorageChanged(): Generator {
|
||||
const ch = channel<React.MouseEvent<HTMLElement>>();
|
||||
|
||||
yield* showSingleton(
|
||||
Level.Info,
|
||||
MessageId.ProgramChanged,
|
||||
undefined,
|
||||
dispatchAction(MessageId.YesReloadProgram, ch.put, 'tick'),
|
||||
ch.close,
|
||||
);
|
||||
|
||||
// if the notification is dismissed without clicking on the action, the
|
||||
// saga will be cancelled here
|
||||
yield take(ch);
|
||||
|
||||
yield put(reloadProgram());
|
||||
}
|
||||
|
||||
function* showCompilerError(action: MpyDidFailToCompileAction): Generator {
|
||||
yield* showSingleton(Level.Error, MessageId.MpyError, { errorMessage: action.err });
|
||||
}
|
||||
|
||||
function* addNotification(action: NotificationAddAction): Generator {
|
||||
const { toaster } = (yield getContext('notification')) as NotificationContext;
|
||||
|
||||
toaster.show({
|
||||
intent: mapIntent(action.level as Level),
|
||||
icon: mapIcon(action.level as Level),
|
||||
message: action.message,
|
||||
timeout: 0,
|
||||
action: action.helpUrl ? helpAction(action.helpUrl) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function* showServiceWorkerUpdate(): Generator {
|
||||
yield* showSingleton(
|
||||
Level.Info,
|
||||
MessageId.ServiceWorkerUpdate,
|
||||
undefined,
|
||||
helpAction('https://github.com/pybricks/pybricks-code/issues/102'),
|
||||
);
|
||||
}
|
||||
|
||||
function* showServiceWorkerSuccess(): Generator {
|
||||
yield* showSingleton(Level.Info, MessageId.ServiceWorkerSuccess);
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(
|
||||
BleDeviceActionType.DidFailToConnect,
|
||||
showBleDeviceDidFailToConnectError,
|
||||
);
|
||||
yield takeEvery(
|
||||
BootloaderConnectionActionType.DidFailToConnect,
|
||||
showBootloaderDidFailToConnectError,
|
||||
);
|
||||
yield takeEvery(EditorActionType.StorageChanged, showEditorStorageChanged);
|
||||
yield takeEvery(MpyActionType.DidFailToCompile, showCompilerError);
|
||||
yield takeEvery(NotificationActionType.Add, addNotification);
|
||||
yield takeEvery(ServiceWorkerActionType.DidUpdate, showServiceWorkerUpdate);
|
||||
yield takeEvery(ServiceWorkerActionType.DidSucceed, showServiceWorkerSuccess);
|
||||
}
|
||||
Reference in New Issue
Block a user