mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pybricks/pybricks-code",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0-beta.1",
|
||||
"license": "MIT",
|
||||
"author": "The Pybricks Authors",
|
||||
"repository": {
|
||||
|
||||
+16
-5
@@ -7,8 +7,10 @@ import { Action } from 'redux';
|
||||
|
||||
/** App action types. */
|
||||
export enum AppActionType {
|
||||
/** Reload the app. */
|
||||
Reload = 'app.action.reload',
|
||||
/** The app has just ben started. */
|
||||
Startup = 'app.action.startup',
|
||||
DidStart = 'app.action.didStart',
|
||||
/** Open settings dialog. */
|
||||
OpenSettings = 'app.action.openSettings',
|
||||
/** Close settings dialog. */
|
||||
@@ -23,12 +25,20 @@ export enum AppActionType {
|
||||
CloseLicenseDialog = 'app.action.closeLicenseDialog',
|
||||
}
|
||||
|
||||
/** Action that requests the app to reload. */
|
||||
export type AppReloadAction = Action<AppActionType.Reload>;
|
||||
|
||||
/** Creates an action that requests the app to reload. */
|
||||
export function reload(): AppReloadAction {
|
||||
return { type: AppActionType.Reload };
|
||||
}
|
||||
|
||||
/** Action that indicates the app has just started. */
|
||||
export type AppStartupAction = Action<AppActionType.Startup>;
|
||||
export type AppDidStartAction = Action<AppActionType.DidStart>;
|
||||
|
||||
/** Creates an action that indicates the app has just started. */
|
||||
export function startup(): AppStartupAction {
|
||||
return { type: AppActionType.Startup };
|
||||
export function didStart(): AppDidStartAction {
|
||||
return { type: AppActionType.DidStart };
|
||||
}
|
||||
|
||||
/** Action to open the settings dialog. */
|
||||
@@ -81,7 +91,8 @@ export function closeLicenseDialog(): AppCloseLicenseDialogAction {
|
||||
|
||||
/** common type for all app actions. */
|
||||
export type AppAction =
|
||||
| AppStartupAction
|
||||
| AppReloadAction
|
||||
| AppDidStartAction
|
||||
| AppOpenSettingsAction
|
||||
| AppCloseSettingsAction
|
||||
| AppOpenAboutDialogAction
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,6 +23,8 @@ import en from './about-i18n.en.json';
|
||||
|
||||
import './about.scss';
|
||||
|
||||
const version = process.env.REACT_APP_VERSION;
|
||||
|
||||
type StateProps = { showAboutDialog: boolean };
|
||||
|
||||
type DispatchProps = { onClose: () => void; onLicenseButtonClick: () => void };
|
||||
@@ -33,7 +35,11 @@ class AboutDialog extends React.Component<AboutDialogProps> {
|
||||
render(): JSX.Element {
|
||||
const { i18n, showAboutDialog, onClose, onLicenseButtonClick } = this.props;
|
||||
return (
|
||||
<Dialog title={appName} isOpen={showAboutDialog} onClose={() => onClose()}>
|
||||
<Dialog
|
||||
title={`${appName} v${version}`}
|
||||
isOpen={showAboutDialog}
|
||||
onClose={() => onClose()}
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<div className="pb-about-icon">
|
||||
<img src="favicon.ico" />
|
||||
|
||||
@@ -78,7 +78,7 @@ class Editor extends React.Component<EditorProps> {
|
||||
render(): JSX.Element {
|
||||
const { darkMode, i18n, onSessionChanged } = this.props;
|
||||
return (
|
||||
<div className="h-100 watermark">
|
||||
<div className="h-100">
|
||||
<ResizeSensor onResize={(): void => this.editor?.resize()}>
|
||||
<AceEditor
|
||||
ref={this.editorRef}
|
||||
|
||||
@@ -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);
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about": {
|
||||
"description": "A simple web app for programming LEGO® Powered Up smart hubs using Pybricks MicroPython.",
|
||||
"description": "MicroPython for LEGO® Powered Up smart hubs.",
|
||||
"licenseButton": { "label": "Open Source Licenses" },
|
||||
"websiteButton": { "label": "Pybricks Website" }
|
||||
}
|
||||
|
||||
@@ -5,17 +5,19 @@
|
||||
|
||||
@import '../variables.scss';
|
||||
|
||||
// make ace editor match app backgound color
|
||||
|
||||
.#{$ns}-dark .ace_gutter {
|
||||
// make ace editor match app backgound color
|
||||
background-color: $pt-dark-app-background-color;
|
||||
}
|
||||
|
||||
.ace_gutter {
|
||||
// make ace editor match app backgound color
|
||||
background-color: $pt-app-background-color;
|
||||
}
|
||||
|
||||
.watermark::after {
|
||||
// add "BETA" watermark
|
||||
|
||||
.pb-beta .ace_scroller::after {
|
||||
content: "";
|
||||
background: url("./images/beta.svg");
|
||||
opacity: 1;
|
||||
|
||||
@@ -6,11 +6,18 @@
|
||||
"connectFailed": "Unexpected error while trying to connect. Check console log and report the error."
|
||||
},
|
||||
"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"
|
||||
"programChanged": {
|
||||
"message": "The program was changed in another window. Do you want to delete this program and replace it with the new program?",
|
||||
"action": "Reload"
|
||||
}
|
||||
},
|
||||
"mpy": {
|
||||
"error": "{errorMessage}"
|
||||
},
|
||||
"serviceWorker": {
|
||||
"success": "Content is cached for offline use.",
|
||||
"update": "New content is available and will be used when all tabs for this page are closed.'"
|
||||
"update": {
|
||||
"message": "A new version of {appName} is available. Click {action} to start using the new version.",
|
||||
"action": "Restart"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ export enum MessageId {
|
||||
BleGattPermission = 'ble.gattPermission',
|
||||
BleGattServiceNotFound = 'ble.gattServiceNotFound',
|
||||
BleNoWebBluetooth = 'ble.noWebBluetooth',
|
||||
ProgramChanged = 'editor.programChanged',
|
||||
ServiceWorkerSuccess = 'serviceWorker.success',
|
||||
ServiceWorkerUpdate = 'serviceWorker.update',
|
||||
YesReloadProgram = 'editor.yesReloadProgram',
|
||||
ProgramChangedMessage = 'editor.programChanged.message',
|
||||
ProgramChangedAction = 'editor.programChanged.action',
|
||||
ServiceWorkerUpdateMessage = 'serviceWorker.update.message',
|
||||
ServiceWorkerUpdateAction = 'serviceWorker.update.action',
|
||||
MpyError = 'mpy.error',
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
body {
|
||||
// no scrolling of the page
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Utility classes
|
||||
@@ -39,6 +40,10 @@ body {
|
||||
|
||||
// global style tweaks
|
||||
|
||||
.#{$ns}-dialog {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.#{$ns}-form-group > .#{$ns}-label {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
+12
-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),
|
||||
@@ -46,6 +48,11 @@ store.subscribe(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// special styling for beta versions
|
||||
if (process.env.REACT_APP_VERSION?.match(/beta/)) {
|
||||
document.body.classList.add('pb-beta');
|
||||
}
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
ReactDOM.render(
|
||||
@@ -64,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 });
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { call, takeEvery } from 'redux-saga/effects';
|
||||
import { AppActionType } from '../actions/app';
|
||||
|
||||
function* reload(): Generator {
|
||||
console.log('reload');
|
||||
|
||||
// unregister the service worker so that when the page reloads, it uses
|
||||
// the new version
|
||||
const registrations = (yield call(() =>
|
||||
navigator.serviceWorker.getRegistrations(),
|
||||
)) as ServiceWorkerRegistration[];
|
||||
|
||||
for (const r of registrations) {
|
||||
yield call(() => r.unregister());
|
||||
}
|
||||
|
||||
location.reload();
|
||||
}
|
||||
|
||||
export default function* app(): Generator {
|
||||
yield takeEvery(AppActionType.Reload, reload);
|
||||
}
|
||||
+7
-3
@@ -1,8 +1,9 @@
|
||||
// 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';
|
||||
import { didStart } from '../actions/app';
|
||||
import app from './app';
|
||||
import bleUart from './ble-uart';
|
||||
import editor from './editor';
|
||||
import errorLog from './error-log';
|
||||
@@ -12,12 +13,14 @@ 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';
|
||||
|
||||
/* istanbul ignore next */
|
||||
export default function* (): Generator {
|
||||
yield all([
|
||||
app(),
|
||||
bleUart(),
|
||||
lwp3BootloaderBle(),
|
||||
lwp3BootloaderProtocol(),
|
||||
@@ -27,8 +30,9 @@ export default function* (): Generator {
|
||||
hub(),
|
||||
license(),
|
||||
mpy(),
|
||||
notification(),
|
||||
settings(),
|
||||
terminal(),
|
||||
put(startup()),
|
||||
put(didStart()),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { IToaster } from '@blueprintjs/core';
|
||||
import { AsyncSaga } from '../../test';
|
||||
import { Action } from '../actions';
|
||||
import {
|
||||
BleDeviceFailToConnectReasonType,
|
||||
didFailToConnect as bleDidFailToConnect,
|
||||
} from '../actions/ble';
|
||||
import { storageChanged } from '../actions/editor';
|
||||
import {
|
||||
BootloaderConnectionFailureReason,
|
||||
didFailToConnect as bootloaderDidFailToConnect,
|
||||
} from '../actions/lwp3-bootloader';
|
||||
import { didFailToCompile } from '../actions/mpy';
|
||||
import { add } from '../actions/notification';
|
||||
import { didSucceed, didUpdate } from '../actions/service-worker';
|
||||
import notification from './notification';
|
||||
|
||||
test.each([
|
||||
bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.NoWebBluetooth }),
|
||||
bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.NoGatt }),
|
||||
bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.NoService }),
|
||||
bleDidFailToConnect({
|
||||
reason: BleDeviceFailToConnectReasonType.Unknown,
|
||||
err: { name: 'test', message: 'unknown' },
|
||||
}),
|
||||
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.Unknown),
|
||||
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.NoWebBluetooth),
|
||||
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.GattServiceNotFound),
|
||||
storageChanged('test'),
|
||||
didFailToCompile('reason'),
|
||||
add('warning', 'message'),
|
||||
add('error', 'message', 'url'),
|
||||
didUpdate({} as ServiceWorkerRegistration),
|
||||
])('actions that should show notification: %o', async (action: Action) => {
|
||||
const getToasts = jest.fn().mockReturnValue([]);
|
||||
const show = jest.fn();
|
||||
const dismiss = jest.fn();
|
||||
const clear = jest.fn();
|
||||
|
||||
const toaster: IToaster = {
|
||||
getToasts,
|
||||
show,
|
||||
dismiss,
|
||||
clear,
|
||||
};
|
||||
|
||||
const saga = new AsyncSaga(notification, { notification: { toaster } });
|
||||
|
||||
saga.put(action);
|
||||
|
||||
expect(show).toBeCalled();
|
||||
expect(dismiss).not.toBeCalled();
|
||||
expect(clear).not.toBeCalled();
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test.each([
|
||||
bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.Canceled }),
|
||||
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.Canceled),
|
||||
didSucceed({} as ServiceWorkerRegistration),
|
||||
])('actions that should not show a notification: %o', async (action: Action) => {
|
||||
const getToasts = jest.fn().mockReturnValue([]);
|
||||
const show = jest.fn();
|
||||
const dismiss = jest.fn();
|
||||
const clear = jest.fn();
|
||||
|
||||
const toaster: IToaster = {
|
||||
getToasts,
|
||||
show,
|
||||
dismiss,
|
||||
clear,
|
||||
};
|
||||
|
||||
const saga = new AsyncSaga(notification, { notification: { toaster } });
|
||||
|
||||
saga.put(action);
|
||||
|
||||
expect(getToasts).not.toBeCalled();
|
||||
expect(show).not.toBeCalled();
|
||||
expect(dismiss).not.toBeCalled();
|
||||
expect(clear).not.toBeCalled();
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
// 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 { reload } from '../actions/app';
|
||||
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';
|
||||
import { appName } from '../settings/ui';
|
||||
|
||||
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.ProgramChangedMessage,
|
||||
undefined,
|
||||
dispatchAction(MessageId.ProgramChangedAction, 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 {
|
||||
const ch = channel<React.MouseEvent<HTMLElement>>();
|
||||
const action = dispatchAction(
|
||||
MessageId.ServiceWorkerUpdateAction,
|
||||
ch.put,
|
||||
'refresh',
|
||||
);
|
||||
yield* showSingleton(
|
||||
Level.Info,
|
||||
MessageId.ServiceWorkerUpdateMessage,
|
||||
{
|
||||
appName,
|
||||
action: React.createElement('strong', undefined, action.text),
|
||||
},
|
||||
action,
|
||||
ch.close,
|
||||
);
|
||||
|
||||
yield take(ch);
|
||||
|
||||
yield put(reload());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
+10
-10
@@ -4,7 +4,7 @@
|
||||
// Tests for settings sagas.
|
||||
|
||||
import { AsyncSaga } from '../../test';
|
||||
import { startup } from '../actions/app';
|
||||
import { didStart } from '../actions/app';
|
||||
import { didBooleanChange, didFailToSetBoolean, setBoolean } from '../actions/settings';
|
||||
import { SettingsState } from '../reducers/settings';
|
||||
import { SettingId } from '../settings/user';
|
||||
@@ -25,7 +25,7 @@ describe('startup', () => {
|
||||
).mockReturnValue(null);
|
||||
innerWidth = 1024;
|
||||
|
||||
saga.put(startup());
|
||||
saga.put(didStart());
|
||||
|
||||
// does nothing
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('startup', () => {
|
||||
).mockReturnValue(null);
|
||||
innerWidth = 800;
|
||||
|
||||
saga.put(startup());
|
||||
saga.put(didStart());
|
||||
|
||||
// does nothing
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('startup', () => {
|
||||
});
|
||||
innerWidth = 1024;
|
||||
|
||||
saga.put(startup());
|
||||
saga.put(didStart());
|
||||
|
||||
// does nothing
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('startup', () => {
|
||||
});
|
||||
innerWidth = 800;
|
||||
|
||||
saga.put(startup());
|
||||
saga.put(didStart());
|
||||
|
||||
// requests documentation to be shown
|
||||
const action = await saga.take();
|
||||
@@ -112,7 +112,7 @@ describe('startup', () => {
|
||||
});
|
||||
innerWidth = 1024;
|
||||
|
||||
saga.put(startup());
|
||||
saga.put(didStart());
|
||||
|
||||
// requests documentation to be hidden
|
||||
const action = await saga.take();
|
||||
@@ -137,7 +137,7 @@ describe('startup', () => {
|
||||
});
|
||||
innerWidth = 800;
|
||||
|
||||
saga.put(startup());
|
||||
saga.put(didStart());
|
||||
|
||||
// does nothing
|
||||
|
||||
@@ -154,7 +154,7 @@ describe('startup', () => {
|
||||
'getItem',
|
||||
).mockReturnValue(null);
|
||||
|
||||
saga.put(startup());
|
||||
saga.put(didStart());
|
||||
|
||||
// does nothing
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('startup', () => {
|
||||
}
|
||||
});
|
||||
|
||||
saga.put(startup());
|
||||
saga.put(didStart());
|
||||
|
||||
// requests to enable dark mode
|
||||
const action = await saga.take();
|
||||
@@ -200,7 +200,7 @@ describe('startup', () => {
|
||||
}
|
||||
});
|
||||
|
||||
saga.put(startup());
|
||||
saga.put(didStart());
|
||||
|
||||
// does nothing
|
||||
|
||||
|
||||
@@ -101,6 +101,6 @@ function* storeSetting(action: SettingsSetBooleanAction): Generator {
|
||||
|
||||
export default function* (): Generator {
|
||||
yield fork(monitorLocalStorage);
|
||||
yield takeEvery(AppActionType.Startup, loadSettings);
|
||||
yield takeEvery(AppActionType.DidStart, loadSettings);
|
||||
yield takeEvery(SettingsActionType.SetBoolean, storeSetting);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { AsyncSaga, delay } from '../../test';
|
||||
|
||||
import { startup } from '../actions/app';
|
||||
import { didStart } from '../actions/app';
|
||||
import {
|
||||
BleUartActionType,
|
||||
BleUartWriteAction,
|
||||
@@ -295,7 +295,7 @@ describe('Data receiver filters out hub status', () => {
|
||||
test('Terminal data source responds to send data actions', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
|
||||
saga.put(startup());
|
||||
saga.put(didStart());
|
||||
const dataSourceAction = await saga.take();
|
||||
expect(dataSourceAction.type).toBe(TerminalActionType.SetDataSource);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from 'redux-saga/effects';
|
||||
import PushStream from 'zen-push';
|
||||
import { Action } from '../actions';
|
||||
import { AppActionType, AppStartupAction } from '../actions/app';
|
||||
import { AppActionType, AppDidStartAction } from '../actions/app';
|
||||
import {
|
||||
BleUartActionType,
|
||||
BleUartNotifyAction,
|
||||
@@ -36,7 +36,7 @@ const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
const terminalDataSource = new PushStream<string>();
|
||||
|
||||
function* startup(_action: AppStartupAction): Generator {
|
||||
function* startup(_action: AppDidStartAction): Generator {
|
||||
yield put(setDataSource(terminalDataSource.observable));
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ function sendTerminalData(action: TerminalDataReceiveDataAction): void {
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(AppActionType.Startup, startup);
|
||||
yield takeEvery(AppActionType.DidStart, startup);
|
||||
yield takeEvery(BleUartActionType.Notify, receiveUartData);
|
||||
yield fork(receiveTerminalData);
|
||||
yield takeEvery(TerminalActionType.SendData, sendTerminalData);
|
||||
|
||||
+2
-1
@@ -12,7 +12,7 @@ export class AsyncSaga {
|
||||
private state: Partial<RootState>;
|
||||
private task: Task;
|
||||
|
||||
public constructor(saga: Saga) {
|
||||
public constructor(saga: Saga, context?: Record<string, unknown>) {
|
||||
this.channel = stdChannel();
|
||||
this.dispatches = [];
|
||||
this.takers = [];
|
||||
@@ -25,6 +25,7 @@ export class AsyncSaga {
|
||||
onError: (e, _i): void => {
|
||||
throw e;
|
||||
},
|
||||
context,
|
||||
},
|
||||
saga,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user