From ff34e5b01b6a7d251b7c3c6e0025e6602470eeb5 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 17 Jan 2021 15:21:10 -0600 Subject: [PATCH 01/10] Add version in about dialog --- .env | 1 + package.json | 2 +- src/components/AboutDialog.tsx | 8 +++++++- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 00000000..0454adfc --- /dev/null +++ b/.env @@ -0,0 +1 @@ +REACT_APP_VERSION=$npm_package_version diff --git a/package.json b/package.json index 4223d04b..aba96565 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/components/AboutDialog.tsx b/src/components/AboutDialog.tsx index f7b506dc..a53f0ffc 100644 --- a/src/components/AboutDialog.tsx +++ b/src/components/AboutDialog.tsx @@ -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 { render(): JSX.Element { const { i18n, showAboutDialog, onClose, onLicenseButtonClick } = this.props; return ( - onClose()}> + onClose()} + >
From 43cc1bce1c5c0178e81a202435188faf08269e14 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 17 Jan 2021 15:41:57 -0600 Subject: [PATCH 02/10] make "BETA" watermark depend on app version --- src/components/Editor.tsx | 2 +- src/components/editor.scss | 8 +++++--- src/index.tsx | 5 +++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 75449edf..669fd12b 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -78,7 +78,7 @@ class Editor extends React.Component { render(): JSX.Element { const { darkMode, i18n, onSessionChanged } = this.props; return ( -
+
this.editor?.resize()}> { } }); +// special styling for beta versions +if (process.env.REACT_APP_VERSION?.match(/beta/)) { + document.body.classList.add('pb-beta'); +} + sagaMiddleware.run(rootSaga); ReactDOM.render( From fc6ee4ff78e0dcfbb2d0ccca1177d8d6d8098b98 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 17 Jan 2021 15:59:11 -0600 Subject: [PATCH 03/10] disable selecting (outside of editor and terminal) --- src/index.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.scss b/src/index.scss index d447317d..a7ad1e20 100644 --- a/src/index.scss +++ b/src/index.scss @@ -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; } From 54439a4cbc41385204cc3c0c896300a456249157 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 18 Jan 2021 16:14:25 -0600 Subject: [PATCH 04/10] 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. --- src/actions/notification.ts | 36 +--- src/components/I18nToaster.tsx | 36 ++++ src/components/Notification.tsx | 121 ++--------- src/components/NotificationStack.tsx | 42 ---- src/components/notification-i18n.en.json | 5 +- src/components/notification-i18n.ts | 1 + src/index.tsx | 13 +- src/reducers/index.ts | 3 - src/reducers/notification.ts | 169 --------------- src/sagas/index.ts | 4 +- src/sagas/notification.ts | 258 +++++++++++++++++++++++ 11 files changed, 325 insertions(+), 363 deletions(-) create mode 100644 src/components/I18nToaster.tsx delete mode 100644 src/components/NotificationStack.tsx delete mode 100644 src/reducers/notification.ts create mode 100644 src/sagas/notification.ts diff --git a/src/actions/notification.ts b/src/actions/notification.ts index 9ce438b3..cf2def5f 100644 --- a/src/actions/notification.ts +++ b/src/actions/notification.ts @@ -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 & { - /** - * Unique ID for this notification instance. - */ - readonly id: number; /** * The type of notification. */ @@ -36,17 +25,6 @@ export type NotificationAddAction = Action & { readonly helpUrl?: string; }; -export type NotificationRemoveAction = Action & { - /** - * 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; diff --git a/src/components/I18nToaster.tsx b/src/components/I18nToaster.tsx new file mode 100644 index 00000000..d7094575 --- /dev/null +++ b/src/components/I18nToaster.tsx @@ -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(); + + ReactDOM.render( + + + , + 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; +} diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index 7f87cb3f..7a2edbee 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -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 { - render(): JSX.Element { - const { - action, - helpUrl, - i18n, - level, - message, - messageId, - onAction, - onClose, - replacements, - } = this.props; - return ( - onClose()} - timeout={0} - intent={mapIntent(level)} - icon={mapIcon(level)} - message={ -
-

- {messageId - ? i18n.translate(messageId, replacements) - : message || 'missing message!'} -

- {helpUrl && ( -

- - More info - -

- )} -
- } - 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)}; +} diff --git a/src/components/NotificationStack.tsx b/src/components/NotificationStack.tsx deleted file mode 100644 index 415a2809..00000000 --- a/src/components/NotificationStack.tsx +++ /dev/null @@ -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 { - render(): JSX.Element { - return ( - - {this.props.list.map((n) => ( - - ))} - - ); - } -} - -const mapStateToProps = (state: RootState): StateProps => ({ - list: state.notification.list, -}); - -export default connect(mapStateToProps)(NotificationStack); diff --git a/src/components/notification-i18n.en.json b/src/components/notification-i18n.en.json index 197d5a7d..bdf93b8a 100644 --- a/src/components/notification-i18n.en.json +++ b/src/components/notification-i18n.en.json @@ -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.", diff --git a/src/components/notification-i18n.ts b/src/components/notification-i18n.ts index 9d4fe68c..75192cd5 100644 --- a/src/components/notification-i18n.ts +++ b/src/components/notification-i18n.ts @@ -12,4 +12,5 @@ export enum MessageId { ServiceWorkerSuccess = 'serviceWorker.success', ServiceWorkerUpdate = 'serviceWorker.update', YesReloadProgram = 'editor.yesReloadProgram', + MpyError = 'mpy.error', } diff --git a/src/index.tsx b/src/index.tsx index 3fb2796a..ba9e496e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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( >
- diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 940b7bf4..25c65740 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -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, diff --git a/src/reducers/notification.ts b/src/reducers/notification.ts deleted file mode 100644 index 690fa1ab..00000000 --- a/src/reducers/notification.ts +++ /dev/null @@ -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; - -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 = (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 }); diff --git a/src/sagas/index.ts b/src/sagas/index.ts index 7682125d..fc2b5626 100644 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -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()), diff --git a/src/sagas/notification.ts b/src/sagas/notification.ts new file mode 100644 index 00000000..aafec817 --- /dev/null +++ b/src/sagas/notification.ts @@ -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) => 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>(); + + 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); +} From f72c6ad5dec0faf29881498fcf5abf4bb650420c Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 18 Jan 2021 16:20:50 -0600 Subject: [PATCH 05/10] rename app startup action using do/did pattern --- src/actions/app.ts | 10 +++++----- src/sagas/index.ts | 4 ++-- src/sagas/settings.test.ts | 20 ++++++++++---------- src/sagas/settings.ts | 2 +- src/sagas/terminal.test.ts | 4 ++-- src/sagas/terminal.ts | 6 +++--- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/actions/app.ts b/src/actions/app.ts index f6bda357..49c1d161 100644 --- a/src/actions/app.ts +++ b/src/actions/app.ts @@ -8,7 +8,7 @@ import { Action } from 'redux'; /** App action types. */ export enum AppActionType { /** The app has just ben started. */ - Startup = 'app.action.startup', + DidStart = 'app.action.didStart', /** Open settings dialog. */ OpenSettings = 'app.action.openSettings', /** Close settings dialog. */ @@ -24,11 +24,11 @@ export enum AppActionType { } /** Action that indicates the app has just started. */ -export type AppStartupAction = Action; +export type AppDidStartAction = Action; /** 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 +81,7 @@ export function closeLicenseDialog(): AppCloseLicenseDialogAction { /** common type for all app actions. */ export type AppAction = - | AppStartupAction + | AppDidStartAction | AppOpenSettingsAction | AppCloseSettingsAction | AppOpenAboutDialogAction diff --git a/src/sagas/index.ts b/src/sagas/index.ts index fc2b5626..16e2a986 100644 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -2,7 +2,7 @@ // 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 bleUart from './ble-uart'; import editor from './editor'; import errorLog from './error-log'; @@ -31,6 +31,6 @@ export default function* (): Generator { notification(), settings(), terminal(), - put(startup()), + put(didStart()), ]); } diff --git a/src/sagas/settings.test.ts b/src/sagas/settings.test.ts index 5875b79f..6e0edb58 100644 --- a/src/sagas/settings.test.ts +++ b/src/sagas/settings.test.ts @@ -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 diff --git a/src/sagas/settings.ts b/src/sagas/settings.ts index 4a6dc695..b39411d6 100644 --- a/src/sagas/settings.ts +++ b/src/sagas/settings.ts @@ -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); } diff --git a/src/sagas/terminal.test.ts b/src/sagas/terminal.test.ts index 528e9d95..6673b7df 100644 --- a/src/sagas/terminal.test.ts +++ b/src/sagas/terminal.test.ts @@ -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); diff --git a/src/sagas/terminal.ts b/src/sagas/terminal.ts index 790a7121..141ed9ec 100644 --- a/src/sagas/terminal.ts +++ b/src/sagas/terminal.ts @@ -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(); -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); From da298ab565bbfecf9039cfd116931910d5f6eadc Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 18 Jan 2021 17:25:59 -0600 Subject: [PATCH 06/10] add some tests for notification sagas --- src/sagas/notification.test.ts | 88 ++++++++++++++++++++++++++++++++++ test/index.ts | 3 +- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/sagas/notification.test.ts diff --git a/src/sagas/notification.test.ts b/src/sagas/notification.test.ts new file mode 100644 index 00000000..60bb024b --- /dev/null +++ b/src/sagas/notification.test.ts @@ -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), + didSucceed({} 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), +])('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(); +}); diff --git a/test/index.ts b/test/index.ts index 5e54bb9c..e5a7c588 100644 --- a/test/index.ts +++ b/test/index.ts @@ -12,7 +12,7 @@ export class AsyncSaga { private state: Partial; private task: Task; - public constructor(saga: Saga) { + public constructor(saga: Saga, context?: Record) { this.channel = stdChannel(); this.dispatches = []; this.takers = []; @@ -25,6 +25,7 @@ export class AsyncSaga { onError: (e, _i): void => { throw e; }, + context, }, saga, ); From c8d499e9790b1113ea844e4884e9251ac7366035 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 18 Jan 2021 17:44:26 -0600 Subject: [PATCH 07/10] drop notification for service worker success action This isn't particularly useful information. --- src/components/notification-i18n.en.json | 1 - src/components/notification-i18n.ts | 1 - src/sagas/notification.test.ts | 2 +- src/sagas/notification.ts | 5 ----- 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/notification-i18n.en.json b/src/components/notification-i18n.en.json index bdf93b8a..8eafe558 100644 --- a/src/components/notification-i18n.en.json +++ b/src/components/notification-i18n.en.json @@ -13,7 +13,6 @@ "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.'" } } diff --git a/src/components/notification-i18n.ts b/src/components/notification-i18n.ts index 75192cd5..c6a157a3 100644 --- a/src/components/notification-i18n.ts +++ b/src/components/notification-i18n.ts @@ -9,7 +9,6 @@ export enum MessageId { BleGattServiceNotFound = 'ble.gattServiceNotFound', BleNoWebBluetooth = 'ble.noWebBluetooth', ProgramChanged = 'editor.programChanged', - ServiceWorkerSuccess = 'serviceWorker.success', ServiceWorkerUpdate = 'serviceWorker.update', YesReloadProgram = 'editor.yesReloadProgram', MpyError = 'mpy.error', diff --git a/src/sagas/notification.test.ts b/src/sagas/notification.test.ts index 60bb024b..2838c9b2 100644 --- a/src/sagas/notification.test.ts +++ b/src/sagas/notification.test.ts @@ -34,7 +34,6 @@ test.each([ add('warning', 'message'), add('error', 'message', 'url'), didUpdate({} as ServiceWorkerRegistration), - didSucceed({} as ServiceWorkerRegistration), ])('actions that should show notification: %o', async (action: Action) => { const getToasts = jest.fn().mockReturnValue([]); const show = jest.fn(); @@ -62,6 +61,7 @@ test.each([ 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(); diff --git a/src/sagas/notification.ts b/src/sagas/notification.ts index aafec817..4af52ee9 100644 --- a/src/sagas/notification.ts +++ b/src/sagas/notification.ts @@ -237,10 +237,6 @@ function* showServiceWorkerUpdate(): Generator { ); } -function* showServiceWorkerSuccess(): Generator { - yield* showSingleton(Level.Info, MessageId.ServiceWorkerSuccess); -} - export default function* (): Generator { yield takeEvery( BleDeviceActionType.DidFailToConnect, @@ -254,5 +250,4 @@ export default function* (): Generator { yield takeEvery(MpyActionType.DidFailToCompile, showCompilerError); yield takeEvery(NotificationActionType.Add, addNotification); yield takeEvery(ServiceWorkerActionType.DidUpdate, showServiceWorkerUpdate); - yield takeEvery(ServiceWorkerActionType.DidSucceed, showServiceWorkerSuccess); } From 5a66f041f2d59f21c45b4b373a711ae63eaa90ee Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 18 Jan 2021 18:51:38 -0600 Subject: [PATCH 08/10] update new version experience This changes the new version message to be more user-friendly and allows using the new version without restarting the browser or clearing the cache. --- src/actions/app.ts | 11 +++++++++++ src/components/notification-i18n.en.json | 5 ++++- src/components/notification-i18n.ts | 3 ++- src/sagas/app.ts | 25 ++++++++++++++++++++++++ src/sagas/index.ts | 2 ++ src/sagas/notification.ts | 22 ++++++++++++++++++--- 6 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 src/sagas/app.ts diff --git a/src/actions/app.ts b/src/actions/app.ts index 49c1d161..e4143d25 100644 --- a/src/actions/app.ts +++ b/src/actions/app.ts @@ -7,6 +7,8 @@ import { Action } from 'redux'; /** App action types. */ export enum AppActionType { + /** Reload the app. */ + Reload = 'app.action.reload', /** The app has just ben started. */ DidStart = 'app.action.didStart', /** Open settings dialog. */ @@ -23,6 +25,14 @@ export enum AppActionType { CloseLicenseDialog = 'app.action.closeLicenseDialog', } +/** Action that requests the app to reload. */ +export type AppReloadAction = Action; + +/** 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 AppDidStartAction = Action; @@ -81,6 +91,7 @@ export function closeLicenseDialog(): AppCloseLicenseDialogAction { /** common type for all app actions. */ export type AppAction = + | AppReloadAction | AppDidStartAction | AppOpenSettingsAction | AppCloseSettingsAction diff --git a/src/components/notification-i18n.en.json b/src/components/notification-i18n.en.json index 8eafe558..f4e861be 100644 --- a/src/components/notification-i18n.en.json +++ b/src/components/notification-i18n.en.json @@ -13,6 +13,9 @@ "error": "{errorMessage}" }, "serviceWorker": { - "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" + } } } diff --git a/src/components/notification-i18n.ts b/src/components/notification-i18n.ts index c6a157a3..b28abff2 100644 --- a/src/components/notification-i18n.ts +++ b/src/components/notification-i18n.ts @@ -9,7 +9,8 @@ export enum MessageId { BleGattServiceNotFound = 'ble.gattServiceNotFound', BleNoWebBluetooth = 'ble.noWebBluetooth', ProgramChanged = 'editor.programChanged', - ServiceWorkerUpdate = 'serviceWorker.update', + ServiceWorkerUpdateMessage = 'serviceWorker.update.message', + ServiceWorkerUpdateAction = 'serviceWorker.update.action', YesReloadProgram = 'editor.yesReloadProgram', MpyError = 'mpy.error', } diff --git a/src/sagas/app.ts b/src/sagas/app.ts new file mode 100644 index 00000000..ceae5066 --- /dev/null +++ b/src/sagas/app.ts @@ -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); +} diff --git a/src/sagas/index.ts b/src/sagas/index.ts index 16e2a986..1e88d206 100644 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -3,6 +3,7 @@ import { all, put } from 'redux-saga/effects'; import { didStart } from '../actions/app'; +import app from './app'; import bleUart from './ble-uart'; import editor from './editor'; import errorLog from './error-log'; @@ -19,6 +20,7 @@ import terminal from './terminal'; /* istanbul ignore next */ export default function* (): Generator { yield all([ + app(), bleUart(), lwp3BootloaderBle(), lwp3BootloaderProtocol(), diff --git a/src/sagas/notification.ts b/src/sagas/notification.ts index 4af52ee9..7a947e91 100644 --- a/src/sagas/notification.ts +++ b/src/sagas/notification.ts @@ -14,6 +14,7 @@ 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, @@ -30,6 +31,7 @@ import { NotificationActionType, NotificationAddAction } from '../actions/notifi 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; @@ -229,12 +231,26 @@ function* addNotification(action: NotificationAddAction): Generator { } function* showServiceWorkerUpdate(): Generator { + const ch = channel>(); + const action = dispatchAction( + MessageId.ServiceWorkerUpdateAction, + ch.put, + 'refresh', + ); yield* showSingleton( Level.Info, - MessageId.ServiceWorkerUpdate, - undefined, - helpAction('https://github.com/pybricks/pybricks-code/issues/102'), + MessageId.ServiceWorkerUpdateMessage, + { + appName, + action: React.createElement('strong', undefined, action.text), + }, + action, + ch.close, ); + + yield take(ch); + + yield put(reload()); } export default function* (): Generator { From f87f9b1b89ecf01fe9889526da1f073ec5d40345 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 18 Jan 2021 18:56:02 -0600 Subject: [PATCH 09/10] associate related programChanged translations --- src/components/notification-i18n.en.json | 6 ++++-- src/components/notification-i18n.ts | 4 ++-- src/sagas/notification.ts | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/notification-i18n.en.json b/src/components/notification-i18n.en.json index f4e861be..90dc0233 100644 --- a/src/components/notification-i18n.en.json +++ b/src/components/notification-i18n.en.json @@ -6,8 +6,10 @@ "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": "Reload" + "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}" diff --git a/src/components/notification-i18n.ts b/src/components/notification-i18n.ts index b28abff2..e5948e18 100644 --- a/src/components/notification-i18n.ts +++ b/src/components/notification-i18n.ts @@ -8,9 +8,9 @@ export enum MessageId { BleGattPermission = 'ble.gattPermission', BleGattServiceNotFound = 'ble.gattServiceNotFound', BleNoWebBluetooth = 'ble.noWebBluetooth', - ProgramChanged = 'editor.programChanged', + ProgramChangedMessage = 'editor.programChanged.message', + ProgramChangedAction = 'editor.programChanged.action', ServiceWorkerUpdateMessage = 'serviceWorker.update.message', ServiceWorkerUpdateAction = 'serviceWorker.update.action', - YesReloadProgram = 'editor.yesReloadProgram', MpyError = 'mpy.error', } diff --git a/src/sagas/notification.ts b/src/sagas/notification.ts index 7a947e91..23b60cbd 100644 --- a/src/sagas/notification.ts +++ b/src/sagas/notification.ts @@ -201,9 +201,9 @@ function* showEditorStorageChanged(): Generator { yield* showSingleton( Level.Info, - MessageId.ProgramChanged, + MessageId.ProgramChangedMessage, undefined, - dispatchAction(MessageId.YesReloadProgram, ch.put, 'tick'), + dispatchAction(MessageId.ProgramChangedAction, ch.put, 'tick'), ch.close, ); From 3e2194a4972fcd3a572996671254e3c5e715fad8 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 18 Jan 2021 18:59:31 -0600 Subject: [PATCH 10/10] simplify about description --- src/components/about-i18n.en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/about-i18n.en.json b/src/components/about-i18n.en.json index 5c9a2b5f..a3f45e2e 100644 --- a/src/components/about-i18n.en.json +++ b/src/components/about-i18n.en.json @@ -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" } }