add better error message when no files to backup

Instead of showing unexpected error, show message explaining that
there are no files to be backed up and how to fix it.

Fixes: https://github.com/pybricks/support/issues/681
This commit is contained in:
David Lechner
2022-07-06 17:56:08 -05:00
parent 1e0d44bbe4
commit 7232bf6153
11 changed files with 96 additions and 21 deletions
+5
View File
@@ -4,9 +4,14 @@
## [Unreleased]
### Added
- Added better error message when no files to backup ([support#681]).
### Fixed
- Fixed deleting files that are not open in the editor.
[support#681]: https://github.com/pybricks/support/issues/681
## [2.0.0-beta.3] - 2022-07-06
### Changed
+3 -1
View File
@@ -15,7 +15,9 @@ export const alertsShowAlert = createAction(
<D extends AlertDomain, S extends AlertSpecific<D>>(
domain: D,
specific: S,
props: AlertProps<D, S>,
...props: AlertProps<D, S> extends never
? [props?: never]
: [props: AlertProps<D, S>]
) => ({
type: 'alerts.action.showAlert',
domain,
+27
View File
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { Icon, Intent } from '@blueprintjs/core';
import React from 'react';
import { CreateToast } from '../../i18nToaster';
import { I18nId, useI18n } from './i18n';
const NoFilesToBackup: React.VoidFunctionComponent = () => {
const i18n = useI18n();
return (
<>
{i18n.translate(I18nId.NoFilesToBackupMessage, {
icon: <Icon icon="plus" />,
})}
</>
);
};
export const noFilesToBackup: CreateToast = (onAction) => {
return {
message: <NoFilesToBackup />,
icon: 'info-sign',
intent: Intent.PRIMARY,
onDismiss: () => onAction('dismiss'),
};
};
+1
View File
@@ -11,4 +11,5 @@ export function useI18n(): I18n {
export enum I18nId {
FileInUseMessage = 'fileInUse.message',
NoFilesToBackupMessage = 'noFilesToBackup.message',
}
+2 -1
View File
@@ -2,6 +2,7 @@
// Copyright (c) 2022 The Pybricks Authors
import { fileInUse } from './FileInUseAlert';
import { noFilesToBackup } from './NoFilesToBackup';
// gathers all of the alert creation functions for passing up to the top level
export default { fileInUse };
export default { fileInUse, noFilesToBackup };
+3
View File
@@ -1,5 +1,8 @@
{
"fileInUse": {
"message": "The file '{fileName}' could not be opened. It is already open in another window."
},
"noFilesToBackup": {
"message": "There are no files to backup. Create a new file first by clicking the {icon} icon."
}
}
+10
View File
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { CustomError } from '../utils/customError';
/** Specific errors for editor subsystem. */
export type ExplorerErrorName = 'NoFiles';
/** Error class for editor subsystem. */
export class ExplorerError extends CustomError<ExplorerErrorName> {}
+31 -1
View File
@@ -70,6 +70,7 @@ import {
duplicateFileDialogDidCancel,
duplicateFileDialogShow,
} from './duplicateFileDialog/actions';
import { ExplorerError, ExplorerErrorName } from './error';
import {
Hub,
newFileWizardDidAccept,
@@ -85,6 +86,23 @@ import explorer from './sagas';
jest.mock('browser-fs-access');
/**
* Asymmetric matcher for matching errors by name while ignoring the message.
* @param name The name to match.
* @returns An asymmetric matcher cast to an ExplorerError so that it can be
* passed to action functions.
*/
function expectExplorerError(name: ExplorerErrorName): ExplorerError {
const matcher: jest.AsymmetricMatcher & Record<string, unknown> = {
$$typeof: Symbol.for('jest.asymmetricMatcher'),
asymmetricMatch: (other) =>
other instanceof ExplorerError && other.name === name,
toAsymmetricMatcher: () => `[ExplorerError: ${name}]`,
};
return matcher as unknown as ExplorerError;
}
describe('handleExplorerArchiveAllFiles', () => {
let saga: AsyncSaga;
@@ -104,6 +122,10 @@ describe('handleExplorerArchiveAllFiles', () => {
saga.put(fileStorageDidFailToDumpAllFiles(testError));
await expect(saga.take()).resolves.toEqual(
alertsShowAlert('alerts', 'unexpectedError', { error: testError }),
);
await expect(saga.take()).resolves.toEqual(
explorerDidFailToArchiveAllFiles(testError),
);
@@ -113,7 +135,11 @@ describe('handleExplorerArchiveAllFiles', () => {
saga.put(fileStorageDidDumpAllFiles([]));
await expect(saga.take()).resolves.toEqual(
explorerDidFailToArchiveAllFiles(new Error('no files')),
alertsShowAlert('explorer', 'noFilesToBackup'),
);
await expect(saga.take()).resolves.toEqual(
explorerDidFailToArchiveAllFiles(expectExplorerError('NoFiles')),
);
});
@@ -133,6 +159,10 @@ describe('handleExplorerArchiveAllFiles', () => {
throw testError;
});
await expect(saga.take()).resolves.toEqual(
alertsShowAlert('alerts', 'unexpectedError', { error: testError }),
);
await expect(saga.take()).resolves.toEqual(
explorerDidFailToArchiveAllFiles(testError),
);
+14 -2
View File
@@ -78,6 +78,7 @@ import {
duplicateFileDialogDidCancel,
duplicateFileDialogShow,
} from './duplicateFileDialog/actions';
import { ExplorerError } from './error';
import {
newFileWizardDidAccept,
newFileWizardDidCancel,
@@ -105,7 +106,7 @@ function* handleExplorerArchiveAllFiles(): Generator {
defined(didDump);
if (didDump.files.length === 0) {
throw new Error('no files');
throw new ExplorerError('NoFiles', 'no files in storage');
}
const zip = new JSZip();
@@ -131,7 +132,18 @@ function* handleExplorerArchiveAllFiles(): Generator {
yield* put(explorerDidArchiveAllFiles());
} catch (err) {
yield* put(explorerDidFailToArchiveAllFiles(ensureError(err)));
const error = ensureError(err);
if (error instanceof ExplorerError && error.name === 'NoFiles') {
yield* put(alertsShowAlert('explorer', 'noFilesToBackup'));
} else {
yield* put(
alertsShowAlert('alerts', 'unexpectedError', {
error,
}),
);
}
yield* put(explorerDidFailToArchiveAllFiles(error));
}
}
-3
View File
@@ -19,7 +19,6 @@ import {
import { editorDidFailToOpenFile } from '../editor/actions';
import { EditorError } from '../editor/error';
import {
explorerDidFailToArchiveAllFiles,
explorerDidFailToCreateNewFile,
explorerDidFailToDeleteFile,
explorerDidFailToDuplicateFile,
@@ -111,7 +110,6 @@ test.each([
appDidCheckForUpdate(false),
bleDIServiceDidReceiveFirmwareRevision('3.0.0'),
fileStorageDidFailToInitialize(new Error('test error')),
explorerDidFailToArchiveAllFiles(new Error('test error')),
explorerDidFailToImportFiles(new Error('test error')),
explorerDidFailToCreateNewFile(new Error('test error')),
explorerDidFailToDuplicateFile('test.file', new Error('test error')),
@@ -137,7 +135,6 @@ test.each([
serviceWorkerDidSucceed(),
appDidCheckForUpdate(true),
bleDIServiceDidReceiveFirmwareRevision(firmwareVersion),
explorerDidFailToArchiveAllFiles(new DOMException('test message', 'AbortError')),
explorerDidFailToImportFiles(new DOMException('test message', 'AbortError')),
explorerDidFailToCreateNewFile(new DOMException('test message', 'AbortError')),
explorerDidFailToDuplicateFile(
-13
View File
@@ -21,7 +21,6 @@ import {
import { editorDidFailToOpenFile } from '../editor/actions';
import { EditorError } from '../editor/error';
import {
explorerDidFailToArchiveAllFiles,
explorerDidFailToCreateNewFile,
explorerDidFailToDeleteFile,
explorerDidFailToDuplicateFile,
@@ -387,17 +386,6 @@ function* showFileStorageFailToInitialize(
yield* showUnexpectedError(I18nId.FileStorageFailedToInitialize, action.error);
}
function* showFileStorageFailToArchive(
action: ReturnType<typeof explorerDidFailToArchiveAllFiles>,
): Generator {
if (action.error.name === 'AbortError') {
// user clicked cancel button - not an error
return;
}
yield* showUnexpectedError(I18nId.ExplorerFailedToArchive, action.error);
}
function* showExplorerFailToImportFiles(
action: ReturnType<typeof explorerDidFailToImportFiles>,
): Generator {
@@ -476,7 +464,6 @@ export default function* (): Generator {
yield* takeEvery(appDidCheckForUpdate, showNoUpdateInfo);
yield* takeEvery(bleDIServiceDidReceiveFirmwareRevision, checkVersion);
yield* takeEvery(fileStorageDidFailToInitialize, showFileStorageFailToInitialize);
yield* takeEvery(explorerDidFailToArchiveAllFiles, showFileStorageFailToArchive);
yield* takeEvery(explorerDidFailToImportFiles, showExplorerFailToImportFiles);
yield* takeEvery(explorerDidFailToCreateNewFile, showExplorerFailToCreateFile);
yield* takeEvery(explorerDidFailToDuplicateFile, showExplorerFailToDuplicate);