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:
David Lechner
2022-10-19 18:26:37 -05:00
committed by David Lechner
parent be9272ca9c
commit fa27649fe3
27 changed files with 121 additions and 141 deletions
+1 -1
View File
@@ -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 = {
+1 -1
View File
@@ -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 = {
+1 -1
View File
@@ -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
View File
@@ -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)}`;
+1 -1
View File
@@ -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 = () => {
+1 -1
View File
@@ -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 = {
+1 -1
View File
@@ -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 = () => {
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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 = {
+1 -1
View File
@@ -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 = {
+1 -1
View File
@@ -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 = () => {
+1 -1
View File
@@ -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 = {
+1 -1
View File
@@ -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 = () => {
+1 -1
View File
@@ -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 = {
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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 = () => {
+1 -1
View File
@@ -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 = () => {
+1 -1
View File
@@ -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
View File
@@ -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
+7 -4
View File
@@ -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) => {
-57
View File
@@ -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
View File
@@ -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>,
+17 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+30
View File
@@ -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 };