mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 10:35:11 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
};
|
||||
@@ -11,4 +11,5 @@ export function useI18n(): I18n {
|
||||
|
||||
export enum I18nId {
|
||||
FileInUseMessage = 'fileInUse.message',
|
||||
NoFilesToBackupMessage = 'noFilesToBackup.message',
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {}
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user