-
-
+
+
+
+
+
+
+
);
};
diff --git a/src/explorer/newFileWizard/actions.ts b/src/explorer/newFileWizard/actions.ts
new file mode 100644
index 00000000..7837137d
--- /dev/null
+++ b/src/explorer/newFileWizard/actions.ts
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+import { createAction } from '../../actions';
+
+import { pythonFileExtension } from '../../pybricksMicropython/lib';
+
+/** Supported file extensions. */
+type SupportedFileExtension = typeof pythonFileExtension;
+
+/** Supported hub types. */
+export enum Hub {
+ /** BOOST Move hub */
+ Move = 'movehub',
+ /** City hub */
+ City = 'cityhub',
+ /** Technic hub */
+ Technic = 'technichub',
+ /** MINDSTORMS Robot Inventor hub */
+ Inventor = 'inventorhub',
+ /** SPIKE Prime hub */
+ Prime = 'primehub',
+ /** SPIKE Essential hub */
+ Essential = 'essentialhub',
+}
+
+export const newFileWizardShow = createAction(() => ({
+ type: 'explorer.newFileWizard.action.show',
+}));
+
+export const newFileWizardDidAccept = createAction(
+ (fileName: string, fileExtension: SupportedFileExtension, hubType: Hub) => ({
+ type: 'explorer.newFileWizard.action.didAccept',
+ fileName,
+ fileExtension,
+ hubType,
+ }),
+);
+
+export const newFileWizardDidCancel = createAction(() => ({
+ type: 'explorer.newFileWizard.action.didCancel',
+}));
diff --git a/src/explorer/newFileWizard/reducers.ts b/src/explorer/newFileWizard/reducers.ts
new file mode 100644
index 00000000..d2be76e9
--- /dev/null
+++ b/src/explorer/newFileWizard/reducers.ts
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+import { Reducer, combineReducers } from 'redux';
+import {
+ newFileWizardDidAccept,
+ newFileWizardDidCancel,
+ newFileWizardShow,
+} from './actions';
+
+/** Controls the new file wizard dialog isOpen state. */
+const isOpen: Reducer
= (state = false, action) => {
+ if (newFileWizardShow.matches(action)) {
+ return true;
+ }
+
+ if (
+ newFileWizardDidAccept.matches(action) ||
+ newFileWizardDidCancel.matches(action)
+ ) {
+ return false;
+ }
+
+ return state;
+};
+
+export default combineReducers({ isOpen });
diff --git a/src/explorer/reducers.ts b/src/explorer/reducers.ts
index cda4c2e7..89c18690 100644
--- a/src/explorer/reducers.ts
+++ b/src/explorer/reducers.ts
@@ -10,6 +10,7 @@ import {
fileStorageDidRemoveItem,
} from '../fileStorage/actions';
+import newFileWizard from './newFileWizard/reducers';
import renameFileDialog from './renameFileDialog/reducers';
export type ExplorerFileInfo = Readonly<{
@@ -51,4 +52,4 @@ const files: Reducer = (state = [], action) => {
return state;
};
-export default combineReducers({ files, renameFileDialog });
+export default combineReducers({ files, newFileWizard, renameFileDialog });
diff --git a/src/explorer/sagas.test.ts b/src/explorer/sagas.test.ts
index eb9121a3..3eb3727e 100644
--- a/src/explorer/sagas.test.ts
+++ b/src/explorer/sagas.test.ts
@@ -25,7 +25,6 @@ import {
} from '../fileStorage/actions';
import { pythonFileExtension } from '../pybricksMicropython/lib';
import {
- Hub,
explorerActivateFile,
explorerArchiveAllFiles,
explorerCreateNewFile,
@@ -35,6 +34,7 @@ import {
explorerDidExportFile,
explorerDidFailToActivateFile,
explorerDidFailToArchiveAllFiles,
+ explorerDidFailToCreateNewFile,
explorerDidFailToExportFile,
explorerDidFailToImportFiles,
explorerDidFailToRenameFile,
@@ -44,6 +44,12 @@ import {
explorerImportFiles,
explorerRenameFile,
} from './actions';
+import {
+ Hub,
+ newFileWizardDidAccept,
+ newFileWizardDidCancel,
+ newFileWizardShow,
+} from './newFileWizard/actions';
import {
renameFileDialogDidAccept,
renameFileDialogDidCancel,
@@ -159,10 +165,28 @@ describe('handleExplorerImportFiles', () => {
});
describe('handleExplorerCreateNewFile', () => {
- it('should dispatch fileStorage action', async () => {
- const saga = new AsyncSaga(explorer);
+ let saga: AsyncSaga;
- saga.put(explorerCreateNewFile('test', pythonFileExtension, Hub.Technic));
+ beforeEach(async () => {
+ saga = new AsyncSaga(explorer);
+
+ saga.put(explorerCreateNewFile());
+
+ await expect(saga.take()).resolves.toEqual(newFileWizardShow());
+ });
+
+ it('should dispatch error when canceled', async () => {
+ saga.put(newFileWizardDidCancel());
+
+ await expect(saga.take()).resolves.toEqual(
+ explorerDidFailToCreateNewFile(
+ new DOMException('user canceled', 'AbortError'),
+ ),
+ );
+ });
+
+ it('should dispatch fileStorage action', async () => {
+ saga.put(newFileWizardDidAccept('test', pythonFileExtension, Hub.Technic));
await expect(saga.take()).resolves.toMatchInlineSnapshot(`
Object {
@@ -182,8 +206,12 @@ describe('handleExplorerCreateNewFile', () => {
saga.put(fileStorageDidWriteFile('test.py'));
- await expect(saga.take()).resolves.toEqual(explorerDidCreateNewFile());
+ await expect(saga.take()).resolves.toEqual(editorActivateFile('test.py'));
+ await expect(saga.take()).resolves.toEqual(explorerDidCreateNewFile());
+ });
+
+ afterEach(async () => {
await saga.end();
});
});
diff --git a/src/explorer/sagas.ts b/src/explorer/sagas.ts
index 08739299..6f0e3170 100644
--- a/src/explorer/sagas.ts
+++ b/src/explorer/sagas.ts
@@ -61,6 +61,11 @@ import {
explorerImportFiles,
explorerRenameFile,
} from './actions';
+import {
+ newFileWizardDidAccept,
+ newFileWizardDidCancel,
+ newFileWizardShow,
+} from './newFileWizard/actions';
import {
renameFileDialogDidAccept,
renameFileDialogDidCancel,
@@ -170,16 +175,27 @@ function* handleExplorerImportFiles(): Generator {
}
}
-function* handleExplorerCreateNewFile(
- action: ReturnType,
-): Generator {
+function* handleExplorerCreateNewFile(): Generator {
try {
- const fileName = `${action.fileName}${action.fileExtension}`;
+ yield* put(newFileWizardShow());
+
+ const { didAccept, didCancel } = yield* race({
+ didAccept: take(newFileWizardDidAccept),
+ didCancel: take(newFileWizardDidCancel),
+ });
+
+ if (didCancel) {
+ throw new DOMException('user canceled', 'AbortError');
+ }
+
+ defined(didAccept);
+
+ const fileName = `${didAccept.fileName}${didAccept.fileExtension}`;
yield* put(
fileStorageWriteFile(
fileName,
- getPybricksMicroPythonFileTemplate(action.hub) || '',
+ getPybricksMicroPythonFileTemplate(didAccept.hubType) || '',
),
);
@@ -194,6 +210,8 @@ function* handleExplorerCreateNewFile(
throw didFailToWrite.error;
}
+ yield* put(editorActivateFile(fileName));
+
yield* put(explorerDidCreateNewFile());
} catch (err) {
yield* put(explorerDidFailToCreateNewFile(ensureError(err)));
diff --git a/src/notifications/sagas.test.ts b/src/notifications/sagas.test.ts
index 523fff64..3f86f825 100644
--- a/src/notifications/sagas.test.ts
+++ b/src/notifications/sagas.test.ts
@@ -139,6 +139,7 @@ test.each([
bleDIServiceDidReceiveFirmwareRevision(firmwareVersion),
explorerDidFailToArchiveAllFiles(new DOMException('test message', 'AbortError')),
explorerDidFailToImportFiles(new DOMException('test message', 'AbortError')),
+ explorerDidFailToCreateNewFile(new DOMException('test message', 'AbortError')),
explorerDidFailToExportFile(
'test.file',
new DOMException('test message', 'AbortError'),
diff --git a/src/notifications/sagas.ts b/src/notifications/sagas.ts
index 9f8f93d6..b80d14ae 100644
--- a/src/notifications/sagas.ts
+++ b/src/notifications/sagas.ts
@@ -448,6 +448,11 @@ function* showExplorerFailToImportFiles(
function* showExplorerFailToCreateFile(
action: ReturnType,
): Generator {
+ if (action.error.name === 'AbortError') {
+ // user clicked cancel button - not an error
+ return;
+ }
+
yield* showUnexpectedError(I18nId.ExplorerFailedToCreate, action.error);
}