diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33cf072e..81e9a1df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@
## [Unreleased]
+### Changed
+- Better error message when download and run fails due to disconnected hub.
+
### Fixed
- Fixed run button active while hub is disconnecting ([support#1021]).
diff --git a/src/ble/alerts/Disconnected.test.tsx b/src/ble/alerts/Disconnected.test.tsx
new file mode 100644
index 00000000..cc872083
--- /dev/null
+++ b/src/ble/alerts/Disconnected.test.tsx
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022-2023 The Pybricks Authors
+
+import { Toast } from '@blueprintjs/core';
+import { act } from '@testing-library/react';
+import React from 'react';
+import { testRender } from '../../../test';
+import { disconnected } from './Disconnected';
+
+it('should dismiss when close is clicked', async () => {
+ const callback = jest.fn();
+ const toast = disconnected(callback, undefined as never);
+
+ const [user, message] = testRender();
+
+ await act(() => user.click(message.getByRole('button', { name: /close/i })));
+
+ expect(callback).toHaveBeenCalledWith('dismiss');
+});
diff --git a/src/ble/alerts/Disconnected.tsx b/src/ble/alerts/Disconnected.tsx
new file mode 100644
index 00000000..93b43a2c
--- /dev/null
+++ b/src/ble/alerts/Disconnected.tsx
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022-2023 The Pybricks Authors
+
+import { Intent } from '@blueprintjs/core';
+import React from 'react';
+import type { CreateToast } from '../../toasterTypes';
+import { useI18n } from './i18n';
+
+const Disconnected: React.VoidFunctionComponent = () => {
+ const i18n = useI18n();
+ return
{i18n.translate('disconnected.message')}
;
+};
+
+export const disconnected: CreateToast = (onAction) => ({
+ message: ,
+ icon: 'error',
+ intent: Intent.DANGER,
+ onDismiss: () => onAction('dismiss'),
+});
diff --git a/src/ble/alerts/index.ts b/src/ble/alerts/index.ts
index 22b7ffc0..18d7641a 100644
--- a/src/ble/alerts/index.ts
+++ b/src/ble/alerts/index.ts
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2022 The Pybricks Authors
+// Copyright (c) 2022-2023 The Pybricks Authors
import { bluetoothNotAvailable } from './BluetoothNotAvailable';
+import { disconnected } from './Disconnected';
import { missingService } from './MissingService';
import { noGatt } from './NoGatt';
import { noHub } from './NoHub';
@@ -11,6 +12,7 @@ import { oldFirmware } from './OldFirmware';
// gathers all of the alert creation functions for passing up to the top level
export default {
bluetoothNotAvailable,
+ disconnected,
missingService,
noGatt,
noHub,
diff --git a/src/ble/alerts/translations/en.json b/src/ble/alerts/translations/en.json
index 97c4da8b..3b12f341 100644
--- a/src/ble/alerts/translations/en.json
+++ b/src/ble/alerts/translations/en.json
@@ -29,5 +29,8 @@
"flashFirmware": {
"label": "Update Pybricks firmware"
}
+ },
+ "disconnected": {
+ "message": "The hub is no longer connected. Please reconnect and try again."
}
}
diff --git a/src/hub/sagas.ts b/src/hub/sagas.ts
index 2bdc5d37..5c789ed5 100644
--- a/src/hub/sagas.ts
+++ b/src/hub/sagas.ts
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2022 The Pybricks Authors
+// Copyright (c) 2020-2023 The Pybricks Authors
import {
SagaGenerator,
@@ -326,9 +326,15 @@ function* handleDownloadAndRun(action: ReturnType): Gener
console.error(err);
}
- yield* put(
- alertsShowAlert('alerts', 'unexpectedError', { error: ensureError(err) }),
- );
+ if (err instanceof DOMException && err.name === 'NetworkError') {
+ yield* put(alertsShowAlert('ble', 'disconnected'));
+ } else {
+ yield* put(
+ alertsShowAlert('alerts', 'unexpectedError', {
+ error: ensureError(err),
+ }),
+ );
+ }
yield* put(didFailToFinishDownload());
}