mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
toasterTypes: change toaster to ref
Instead of rendering the toaster separate from the main index, add it there so we can inherit all of the context providers. This requires passing the ref object instead of the toaster instance itself, so sagas have to be updated.
This commit is contained in:
committed by
David Lechner
parent
be9272ca9c
commit
fa27649fe3
+1
-1
@@ -6,7 +6,7 @@ import alerts from './alerts/alerts';
|
||||
import ble from './ble/alerts';
|
||||
import explorer from './explorer/alerts';
|
||||
import firmware from './firmware/alerts';
|
||||
import { CreateToast } from './i18nToaster';
|
||||
import type { CreateToast } from './toasterTypes';
|
||||
|
||||
/** This collects alerts from all of the subsystems of the app */
|
||||
const alertDomains = {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@blueprintjs/core';
|
||||
import React, { useState } from 'react';
|
||||
import { useId } from 'react-aria';
|
||||
import { CreateToast } from '../i18nToaster';
|
||||
import type { CreateToast } from '../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
type UnexpectedErrorAlertProps = {
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('handleShowAlert', () => {
|
||||
toaster = new TestToaster();
|
||||
jest.spyOn(toaster, 'show');
|
||||
jest.spyOn(toaster, 'dismiss');
|
||||
saga = new AsyncSaga(alerts, { toaster });
|
||||
saga = new AsyncSaga(alerts, { toasterRef: { current: toaster } });
|
||||
});
|
||||
|
||||
it('should show toast', async () => {
|
||||
|
||||
+5
-3
@@ -1,17 +1,19 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { ToasterInstance } from '@blueprintjs/core';
|
||||
import { eventChannel } from 'redux-saga';
|
||||
import { delay, getContext, put, take, takeEvery } from 'typed-redux-saga/macro';
|
||||
import { getAlertProps } from '../alerts';
|
||||
import type { ToasterRef } from '../toasterTypes';
|
||||
import { defined } from '../utils';
|
||||
import { alertsDidShowAlert, alertsShowAlert } from './actions';
|
||||
|
||||
export type AlertsSagaContext = { toaster: ToasterInstance };
|
||||
export type AlertsSagaContext = { toasterRef: ToasterRef };
|
||||
|
||||
/** Shows an alert to the user and avoids duplicate alerts. */
|
||||
function* handleShowAlert(action: ReturnType<typeof alertsShowAlert>): Generator {
|
||||
const toaster = yield* getContext<ToasterInstance>('toaster');
|
||||
const toaster = (yield* getContext<ToasterRef>('toasterRef')).current;
|
||||
defined(toaster);
|
||||
|
||||
const key = `${action.domain}.${action.specific}.${JSON.stringify(action.props)}`;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
const BluetoothNotAvailable: React.VoidFunctionComponent = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
type MissingServiceProps = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
const NoGatt: React.VoidFunctionComponent = () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import './index.scss';
|
||||
import { AnchorButton, Button, Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { appName, pybricksBluetoothTroubleshootingUrl } from '../../app/constants';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import ExternalLinkIcon from '../../utils/ExternalLinkIcon';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Button, Code, Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { isIOS, isLinux } from '../../utils/os';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import './index.scss';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
type OldFirmwareProps = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
type FileInUseAlertProps = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Icon, Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
const NoFilesToBackup: React.VoidFunctionComponent = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
type DfuErrorProps = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
const FirmwareMismatch: React.VoidFunctionComponent = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Intent, ProgressBar } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
type FlashProgressProps = {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { AnchorButton, Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { pybricksUsbDfuTroubleshootingUrl } from '../../app/constants';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import ExternalLinkIcon from '../../utils/ExternalLinkIcon';
|
||||
import { isLinux, isWindows } from '../../utils/os';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
const NoDfuInterface: React.VoidFunctionComponent = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
const NoWebUsb: React.VoidFunctionComponent = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
const ReleaseButton: React.VoidFunctionComponent = () => {
|
||||
|
||||
+23
-23
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
import { ToasterInstance } from '@blueprintjs/core';
|
||||
import type { ToasterInstance } from '@blueprintjs/core';
|
||||
import {
|
||||
FirmwareMetadata,
|
||||
FirmwareMetadataV110,
|
||||
@@ -83,7 +83,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -231,7 +231,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -361,7 +361,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -411,7 +411,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -479,7 +479,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -543,7 +543,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -610,7 +610,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -670,7 +670,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -736,7 +736,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -819,7 +819,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -885,7 +885,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -984,7 +984,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -1092,7 +1092,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -1240,7 +1240,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -1386,7 +1386,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -1538,7 +1538,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -1585,7 +1585,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -1631,7 +1631,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -1691,7 +1691,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -1752,7 +1752,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -1816,7 +1816,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
@@ -1905,7 +1905,7 @@ describe('flashFirmware', () => {
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
toasterRef: { current: mock<ToasterInstance>() },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
|
||||
import { ToasterInstance } from '@blueprintjs/core';
|
||||
import {
|
||||
FirmwareReader,
|
||||
FirmwareReaderError,
|
||||
@@ -56,6 +55,7 @@ import { MaxProgramFlashSize, Result } from '../lwp3-bootloader/protocol';
|
||||
import { BootloaderConnectionState } from '../lwp3-bootloader/reducers';
|
||||
import { compile, didCompile, didFailToCompile } from '../mpy/actions';
|
||||
import { RootState } from '../reducers';
|
||||
import type { ToasterRef } from '../toasterTypes';
|
||||
import { LegoUsbProductId, legoUsbVendorId } from '../usb';
|
||||
import { defined, ensureError, hex, maybe } from '../utils';
|
||||
import { crc32, fmod, sumComplement32 } from '../utils/math';
|
||||
@@ -92,7 +92,8 @@ const firmwareZipMap = new Map<HubType, string>([
|
||||
* parent task).
|
||||
*/
|
||||
function* disconnectAndCancel(): SagaGenerator<void> {
|
||||
const toaster = yield* getContext<ToasterInstance>('toaster');
|
||||
const toaster = (yield* getContext<ToasterRef>('toasterRef')).current;
|
||||
defined(toaster);
|
||||
|
||||
toaster.dismiss('firmware.ble.progress');
|
||||
|
||||
@@ -375,7 +376,8 @@ function* loadFirmware(
|
||||
* @param action The action that triggered this saga.
|
||||
*/
|
||||
function* handleFlashFirmware(action: ReturnType<typeof flashFirmware>): Generator {
|
||||
const toaster = yield* getContext<ToasterInstance>('toaster');
|
||||
const toaster = (yield* getContext<ToasterRef>('toasterRef')).current;
|
||||
defined(toaster);
|
||||
|
||||
try {
|
||||
let firmware: Uint8Array | undefined = undefined;
|
||||
@@ -703,7 +705,8 @@ function* handleFlashUsbDfu(action: ReturnType<typeof firmwareFlashUsbDfu>): Gen
|
||||
dfu.dfuseStartAddress = dfuFirmwareStartAddress;
|
||||
const writeProc = dfu.write(1024, firmware, true);
|
||||
|
||||
const toaster = yield* getContext<ToasterInstance>('toaster');
|
||||
const toaster = (yield* getContext<ToasterRef>('toasterRef')).current;
|
||||
defined(toaster);
|
||||
|
||||
defer.push(
|
||||
writeProc.events.on('erase/process', (sent, total) => {
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
import { ToastProps, Toaster, ToasterInstance } from '@blueprintjs/core';
|
||||
import { I18nContext, I18nManager } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
/**
|
||||
* Creates an `ToasterInstance` 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): ToasterInstance {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template type alert callbacks.
|
||||
*
|
||||
* This is called when an alert is dismissed.
|
||||
*
|
||||
* @param action: The action that the user selected. This is usually 'dismiss'.
|
||||
*/
|
||||
export type ToastActionHandler<A extends string> = (action: A) => void;
|
||||
|
||||
/**
|
||||
* Template type for all toast creation functions for alert components.
|
||||
*
|
||||
* @param onAction A callback that is called when the toast is dismissed.
|
||||
* @param props Additional properties required by this toast, if any (usually
|
||||
* replacements for translations).
|
||||
*/
|
||||
export type CreateToast<
|
||||
P extends Record<string, unknown> = never,
|
||||
A extends string = 'dismiss',
|
||||
> = (onAction: ToastActionHandler<A>, props: P) => ToastProps;
|
||||
+4
-5
@@ -2,7 +2,7 @@
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
|
||||
import './index.scss';
|
||||
import { HotkeysProvider } from '@blueprintjs/core';
|
||||
import { HotkeysProvider, Toaster } from '@blueprintjs/core';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { I18nContext } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
@@ -15,7 +15,6 @@ import App from './app/App';
|
||||
import { appVersion } from './app/constants';
|
||||
import { db } from './fileStorage/context';
|
||||
import { i18nManager } from './i18n';
|
||||
import * as I18nToaster from './i18nToaster';
|
||||
import { rootReducer } from './reducers';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import rootSaga, { RootSagaContext } from './sagas';
|
||||
@@ -23,15 +22,14 @@ import { defaultTerminalContext } from './terminal/TerminalContext';
|
||||
import ViewHeightSensor from './utils/ViewHeightSensor';
|
||||
import { createCountFunc } from './utils/iter';
|
||||
|
||||
const toaster = I18nToaster.create(i18nManager);
|
||||
const toasterRef = React.createRef<Toaster>();
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware<RootSagaContext>({
|
||||
context: {
|
||||
nextMessageId: createCountFunc(),
|
||||
notification: { toaster },
|
||||
terminal: defaultTerminalContext,
|
||||
fileStorage: db,
|
||||
toaster,
|
||||
toasterRef,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -103,6 +101,7 @@ ReactDOM.render(
|
||||
<App />
|
||||
</HotkeysProvider>
|
||||
</OverlayProvider>
|
||||
<Toaster ref={toasterRef} />
|
||||
</I18nContext.Provider>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
import { ToasterInstance } from '@blueprintjs/core';
|
||||
import type { IToastOptions, ToasterInstance } from '@blueprintjs/core';
|
||||
import { FirmwareReaderError, FirmwareReaderErrorCode } from '@pybricks/firmware';
|
||||
import { I18nManager } from '@shopify/react-i18n';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { AnyAction } from 'redux';
|
||||
import { AsyncSaga, uuid } from '../../test';
|
||||
import { appDidCheckForUpdate } from '../app/actions';
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
MetadataProblem,
|
||||
didFailToFinish,
|
||||
} from '../firmware/actions';
|
||||
import * as i18nToaster from '../i18nToaster';
|
||||
import {
|
||||
BootloaderConnectionFailureReason,
|
||||
didFailToConnect as bootloaderDidFailToConnect,
|
||||
@@ -38,15 +37,27 @@ import { I18nId } from './i18n';
|
||||
import notification from './sagas';
|
||||
|
||||
function createTestToasterSaga(): { toaster: ToasterInstance; saga: AsyncSaga } {
|
||||
const i18n = new I18nManager({ locale: 'en' });
|
||||
const toaster = i18nToaster.create(i18n);
|
||||
const toasts = new Map<string, IToastOptions>();
|
||||
|
||||
const toaster = mock<ToasterInstance>({
|
||||
show: (props, key) => {
|
||||
return key ?? '';
|
||||
},
|
||||
dismiss: (key) => {
|
||||
toasts.delete(key);
|
||||
},
|
||||
clear: () => {
|
||||
toasts.clear();
|
||||
},
|
||||
getToasts: () => [...toasts.values()],
|
||||
});
|
||||
|
||||
jest.spyOn(toaster, 'clear');
|
||||
jest.spyOn(toaster, 'dismiss');
|
||||
jest.spyOn(toaster, 'getToasts');
|
||||
jest.spyOn(toaster, 'show');
|
||||
|
||||
const saga = new AsyncSaga(notification, { notification: { toaster } });
|
||||
const saga = new AsyncSaga(notification, { toasterRef: { current: toaster } });
|
||||
|
||||
return { toaster, saga };
|
||||
}
|
||||
|
||||
+16
-23
@@ -3,13 +3,7 @@
|
||||
|
||||
// Saga for managing notifications (toasts)
|
||||
|
||||
import {
|
||||
ActionProps,
|
||||
IconName,
|
||||
Intent,
|
||||
LinkProps,
|
||||
ToasterInstance,
|
||||
} from '@blueprintjs/core';
|
||||
import { ActionProps, IconName, Intent, LinkProps } from '@blueprintjs/core';
|
||||
import { Replacements } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { channel } from 'redux-saga';
|
||||
@@ -34,22 +28,13 @@ import {
|
||||
} from '../lwp3-bootloader/actions';
|
||||
import { didCompile, didFailToCompile } from '../mpy/actions';
|
||||
import { serviceWorkerDidUpdate } from '../service-worker/actions';
|
||||
import type { ToasterRef } from '../toasterTypes';
|
||||
import { defined } from '../utils';
|
||||
import NotificationAction from './NotificationAction';
|
||||
import NotificationMessage from './NotificationMessage';
|
||||
import { add as addNotification } from './actions';
|
||||
import { I18nId } from './i18n';
|
||||
|
||||
type NotificationContext = {
|
||||
toaster: ToasterInstance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Partial saga context type for context used in the notification sagas.
|
||||
*/
|
||||
export type NotificationSagaContext = {
|
||||
notification: NotificationContext;
|
||||
};
|
||||
|
||||
/** Severity level of notification. */
|
||||
enum Level {
|
||||
/** This is an error (requires user action to resolve). */
|
||||
@@ -126,7 +111,8 @@ function* showSingleton(
|
||||
action?: ActionProps & LinkProps,
|
||||
onDismiss?: (didTimeoutExpire: boolean) => void,
|
||||
): Generator {
|
||||
const { toaster } = yield* getContext<NotificationContext>('notification');
|
||||
const toaster = (yield* getContext<ToasterRef>('toasterRef')).current;
|
||||
defined(toaster);
|
||||
|
||||
// if the message is already showing, close it and wait some time so that
|
||||
// users can see that something triggered the message again
|
||||
@@ -158,7 +144,9 @@ function* showSingleton(
|
||||
|
||||
/** Shows a special notification for unexpected errors. */
|
||||
function* showUnexpectedError(messageId: I18nId, error: Error): Generator {
|
||||
const { toaster } = yield* getContext<NotificationContext>('notification');
|
||||
const toaster = (yield* getContext<ToasterRef>('toasterRef')).current;
|
||||
defined(toaster);
|
||||
|
||||
const key = `alerts.unexpectedError.${messageId}`;
|
||||
|
||||
toaster.show(
|
||||
@@ -248,7 +236,9 @@ function* showFlashFirmwareError(
|
||||
}
|
||||
|
||||
function* dismissCompilerError(): Generator {
|
||||
const { toaster } = yield* getContext<NotificationContext>('notification');
|
||||
const toaster = (yield* getContext<ToasterRef>('toasterRef')).current;
|
||||
defined(toaster);
|
||||
|
||||
toaster.dismiss(I18nId.MpyError);
|
||||
}
|
||||
|
||||
@@ -263,7 +253,8 @@ function* showCompilerError(action: ReturnType<typeof didFailToCompile>): Genera
|
||||
}
|
||||
|
||||
function* handleAddNotification(action: ReturnType<typeof addNotification>): Generator {
|
||||
const { toaster } = yield* getContext<NotificationContext>('notification');
|
||||
const toaster = (yield* getContext<ToasterRef>('toasterRef')).current;
|
||||
defined(toaster);
|
||||
|
||||
toaster.show({
|
||||
intent: mapIntent(action.level as Level),
|
||||
@@ -303,7 +294,9 @@ function* showNoUpdateInfo(action: ReturnType<typeof appDidCheckForUpdate>): Gen
|
||||
return;
|
||||
}
|
||||
|
||||
const { toaster } = yield* getContext<NotificationContext>('notification');
|
||||
const toaster = (yield* getContext<ToasterRef>('toasterRef')).current;
|
||||
defined(toaster);
|
||||
|
||||
toaster.show({
|
||||
intent: mapIntent(Level.Info),
|
||||
icon: mapIcon(Level.Info),
|
||||
|
||||
+1
-2
@@ -16,7 +16,7 @@ import hub from './hub/sagas';
|
||||
import lwp3BootloaderProtocol from './lwp3-bootloader/sagas';
|
||||
import lwp3BootloaderBle from './lwp3-bootloader/sagas-ble';
|
||||
import mpy from './mpy/sagas';
|
||||
import notifications, { NotificationSagaContext } from './notifications/sagas';
|
||||
import notifications from './notifications/sagas';
|
||||
import terminal, { TerminalSagaContext } from './terminal/sagas';
|
||||
|
||||
/* istanbul ignore next */
|
||||
@@ -48,5 +48,4 @@ export type RootSagaContext = {
|
||||
nextMessageId: () => number;
|
||||
} & AlertsSagaContext &
|
||||
FileStorageSageContext &
|
||||
NotificationSagaContext &
|
||||
TerminalSagaContext;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
import type { ToastProps, ToasterInstance } from '@blueprintjs/core';
|
||||
|
||||
/**
|
||||
* Template type alert callbacks.
|
||||
*
|
||||
* This is called when an alert is dismissed.
|
||||
*
|
||||
* @param action: The action that the user selected. This is usually 'dismiss'.
|
||||
*/
|
||||
export type ToastActionHandler<A extends string> = (action: A) => void;
|
||||
|
||||
/**
|
||||
* Template type for all toast creation functions for alert components.
|
||||
*
|
||||
* @param onAction A callback that is called when the toast is dismissed.
|
||||
* @param props Additional properties required by this toast, if any (usually
|
||||
* replacements for translations).
|
||||
*/
|
||||
export type CreateToast<
|
||||
P extends Record<string, unknown> = never,
|
||||
A extends string = 'dismiss',
|
||||
> = (onAction: ToastActionHandler<A>, props: P) => ToastProps;
|
||||
|
||||
/**
|
||||
* Type compatible with React.RefObject<ToasterInstance>.
|
||||
*/
|
||||
export type ToasterRef = { current: ToasterInstance | null };
|
||||
Reference in New Issue
Block a user