mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-15 11:04:49 +00:00
rework alerts
This starts moving alerts to the same subsystem where they are relevant instead of putting everything in notifications. So far, only the explorer file in use error is handled like this.
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { IToaster, Toaster } from '@blueprintjs/core';
|
||||
import { I18nContext, I18nManager } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
/**
|
||||
* Creates an `IToaster` for static usage similar to `Toaster.create()` except
|
||||
* that it is wrapped in an `I18nContext.Provider` so that messages can be
|
||||
* translated.
|
||||
*
|
||||
* @param i18n The i18n manager object.
|
||||
*/
|
||||
export function create(i18n: I18nManager): IToaster {
|
||||
const containerElement = document.createElement('div');
|
||||
|
||||
document.body.appendChild(containerElement);
|
||||
|
||||
const toaster = React.createRef<Toaster>();
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nContext.Provider value={i18n}>
|
||||
<Toaster usePortal={false} ref={toaster} />
|
||||
</I18nContext.Provider>,
|
||||
containerElement,
|
||||
);
|
||||
|
||||
// istanbul ignore if: should not happen since we are rendering the component
|
||||
if (toaster.current === null) {
|
||||
throw new Error('failed to set toaster ref');
|
||||
}
|
||||
|
||||
return toaster.current;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
@use '@blueprintjs/core/lib/scss/variables' as bp;
|
||||
|
||||
pre.pb-notification-stack-trace {
|
||||
max-width: bp.$pt-grid-size * 50;
|
||||
max-height: bp.$pt-grid-size * 50;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
// Provides special notification contents for unexpected errors.
|
||||
|
||||
import './UnexpectedErrorNotification.scss';
|
||||
import { AnchorButton, Button, ButtonGroup, Collapse, Intent } from '@blueprintjs/core';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { useId } from 'react-aria';
|
||||
import { I18nId } from './i18n';
|
||||
|
||||
type UnexpectedErrorNotificationProps = {
|
||||
messageId: I18nId;
|
||||
err: Error;
|
||||
};
|
||||
|
||||
const UnexpectedErrorNotification: React.VoidFunctionComponent<
|
||||
UnexpectedErrorNotificationProps
|
||||
> = ({ messageId, err }) => {
|
||||
// istanbul ignore next: babel-loader rewrites this line
|
||||
const [i18n] = useI18n();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const labelId = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{i18n.translate(messageId, { errorMessage: err.message })}</p>
|
||||
<span>
|
||||
<Button
|
||||
aria-labelledby={labelId}
|
||||
minimal={true}
|
||||
small={true}
|
||||
icon={isExpanded ? 'chevron-down' : 'chevron-right'}
|
||||
onClick={() => setIsExpanded((v) => !v)}
|
||||
/>
|
||||
<span id={labelId}>{i18n.translate(I18nId.TechnicalInfo)}</span>
|
||||
</span>
|
||||
<Collapse isOpen={isExpanded}>
|
||||
<pre className="pb-notification-stack-trace">{err.stack}</pre>
|
||||
</Collapse>
|
||||
<div>
|
||||
<ButtonGroup minimal={true} fill={true}>
|
||||
<Button
|
||||
intent={Intent.DANGER}
|
||||
icon="duplicate"
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
`\`\`\`\n${err.stack || err.message}\n\`\`\``,
|
||||
)
|
||||
}
|
||||
>
|
||||
{i18n.translate(I18nId.CopyErrorMessage)}
|
||||
</Button>
|
||||
<AnchorButton
|
||||
intent={Intent.DANGER}
|
||||
icon="virus"
|
||||
href={`https://github.com/pybricks/support/issues?q=${encodeURIComponent(
|
||||
'is:issue',
|
||||
)}+${encodeURIComponent(err.message)}`}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(I18nId.ReportBug)}
|
||||
</AnchorButton>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnexpectedErrorNotification;
|
||||
@@ -5,9 +5,6 @@
|
||||
|
||||
export enum I18nId {
|
||||
AppNoUpdateFound = 'app.noUpdateFound',
|
||||
CopyErrorMessage = 'copyErrorMessage',
|
||||
TechnicalInfo = 'technicalInfo',
|
||||
ReportBug = 'reportBug',
|
||||
BleUnexpectedError = 'ble.unexpectedError',
|
||||
BleGattPermission = 'ble.gattPermission',
|
||||
BleGattServiceNotFound = 'ble.gattServiceNotFound',
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
MetadataProblem,
|
||||
didFailToFinish,
|
||||
} from '../firmware/actions';
|
||||
import * as i18nToaster from '../i18nToaster';
|
||||
import {
|
||||
BootloaderConnectionFailureReason,
|
||||
didFailToConnect as bootloaderDidFailToConnect,
|
||||
@@ -41,14 +42,13 @@ import {
|
||||
serviceWorkerDidSucceed,
|
||||
serviceWorkerDidUpdate,
|
||||
} from '../service-worker/actions';
|
||||
import * as I18nToaster from './I18nToaster';
|
||||
import { add } from './actions';
|
||||
import { I18nId } from './i18n';
|
||||
import notification from './sagas';
|
||||
|
||||
function createTestToasterSaga(): { toaster: IToaster; saga: AsyncSaga } {
|
||||
const i18n = new I18nManager({ locale: 'en' });
|
||||
const toaster = I18nToaster.create(i18n);
|
||||
const toaster = i18nToaster.create(i18n);
|
||||
|
||||
jest.spyOn(toaster, 'clear');
|
||||
jest.spyOn(toaster, 'dismiss');
|
||||
|
||||
+15
-10
@@ -10,6 +10,7 @@ import React from 'react';
|
||||
import { channel } from 'redux-saga';
|
||||
import * as semver from 'semver';
|
||||
import { delay, getContext, put, take, takeEvery } from 'typed-redux-saga/macro';
|
||||
import { getAlertProps } from '../alerts';
|
||||
import { appDidCheckForUpdate, appReload } from '../app/actions';
|
||||
import { appName } from '../app/constants';
|
||||
import { bleDIServiceDidReceiveFirmwareRevision } from '../ble-device-info-service/actions';
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
didFailToConnect as bleDeviceDidFailToConnect,
|
||||
} from '../ble/actions';
|
||||
import { editorDidFailToOpenFile } from '../editor/actions';
|
||||
import { EditorError } from '../editor/error';
|
||||
import {
|
||||
explorerDidFailToArchiveAllFiles,
|
||||
explorerDidFailToCreateNewFile,
|
||||
@@ -37,7 +39,6 @@ import { serviceWorkerDidUpdate } from '../service-worker/actions';
|
||||
import { pythonVersionToSemver } from '../utils/version';
|
||||
import NotificationAction from './NotificationAction';
|
||||
import NotificationMessage from './NotificationMessage';
|
||||
import UnexpectedErrorNotification from './UnexpectedErrorNotification';
|
||||
import { add as addNotification } from './actions';
|
||||
import { I18nId } from './i18n';
|
||||
|
||||
@@ -159,14 +160,15 @@ function* showSingleton(
|
||||
}
|
||||
|
||||
/** Shows a special notification for unexpected errors. */
|
||||
function* showUnexpectedError(messageId: I18nId, err: Error): Generator {
|
||||
function* showUnexpectedError(messageId: I18nId, error: Error): Generator {
|
||||
const { toaster } = yield* getContext<NotificationContext>('notification');
|
||||
toaster.show({
|
||||
intent: mapIntent(Level.Error),
|
||||
icon: mapIcon(Level.Error),
|
||||
message: React.createElement(UnexpectedErrorNotification, { messageId, err }),
|
||||
timeout: 0,
|
||||
});
|
||||
const key = `alerts.unexpectedError.${messageId}`;
|
||||
|
||||
toaster.show(
|
||||
getAlertProps('alerts', 'unexpectedError', () => toaster.dismiss(key), {
|
||||
error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function* showBleDeviceDidFailToConnectError(
|
||||
@@ -441,9 +443,12 @@ function* showExplorerFailToExport(
|
||||
}
|
||||
|
||||
function* showEditorDidFailToOpenFile(
|
||||
action: ReturnType<typeof explorerDidFailToExportFile>,
|
||||
action: ReturnType<typeof editorDidFailToOpenFile>,
|
||||
): Generator {
|
||||
// TODO: add a better error message for the case where a file is already in use
|
||||
if (action.error instanceof EditorError && action.error.name === 'FileInUse') {
|
||||
return;
|
||||
}
|
||||
|
||||
yield* showUnexpectedError(I18nId.EditorFailedToOpenFile, action.error);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"copyErrorMessage": "Copy Error Message",
|
||||
"technicalInfo": "Expand for detailed technical information",
|
||||
"reportBug": "Report Bug",
|
||||
"app": {
|
||||
"noUpdateFound": "{appName} is already up to date."
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user