= ({ i18n }) => {
liveDescriptors={liveDescriptors}
canRename={false} // we implement our own rename handler
onFocusItem={(item) => setFocusedItem(item.index)}
+ onPrimaryAction={(item) =>
+ dispatch(explorerActivateFile(item.data.fileName))
+ }
>
({
error,
}));
+/**
+ * Request to activate a file (open or bring to foreground if already open).
+ * @param fileName The file name.
+ */
+export const explorerActivateFile = createAction((fileName: string) => ({
+ type: 'explorer.action.activateFile',
+ fileName,
+}));
+
+/**
+ * Indicates that {@link explorerActivateFile} succeeded.
+ * @param fileName The file name.
+ */
+export const explorerDidActivateFile = createAction((fileName: string) => ({
+ type: 'explorer.action.didActivateFile',
+ fileName,
+}));
+
+/**
+ * Indicates that {@link explorerActivateFile} failed.
+ * @param fileName The file name.
+ * @param error The error that was raised.
+ */
+export const explorerDidFailToActivateFile = createAction(
+ (fileName: string, error: Error) => ({
+ type: 'explorer.action.didFailToActivateFile',
+ fileName,
+ error,
+ }),
+);
+
/**
* Action that requests to rename a file.
* @param fileName The file name.
diff --git a/src/explorer/sagas.test.ts b/src/explorer/sagas.test.ts
index b5bc2b57..eb9121a3 100644
--- a/src/explorer/sagas.test.ts
+++ b/src/explorer/sagas.test.ts
@@ -5,6 +5,11 @@ import * as browserFsAccess from 'browser-fs-access';
import { FileWithHandle } from 'browser-fs-access';
import { mock } from 'jest-mock-extended';
import { AsyncSaga } from '../../test';
+import {
+ editorActivateFile,
+ editorDidActivateFile,
+ editorDidFailToActivateFile,
+} from '../editor/actions';
import {
fileStorageDidDumpAllFiles,
fileStorageDidFailToDumpAllFiles,
@@ -21,11 +26,14 @@ import {
import { pythonFileExtension } from '../pybricksMicropython/lib';
import {
Hub,
+ explorerActivateFile,
explorerArchiveAllFiles,
explorerCreateNewFile,
+ explorerDidActivateFile,
explorerDidArchiveAllFiles,
explorerDidCreateNewFile,
explorerDidExportFile,
+ explorerDidFailToActivateFile,
explorerDidFailToArchiveAllFiles,
explorerDidFailToExportFile,
explorerDidFailToImportFiles,
@@ -180,6 +188,39 @@ describe('handleExplorerCreateNewFile', () => {
});
});
+describe('handleExplorerActivateFile', () => {
+ let saga: AsyncSaga;
+
+ beforeEach(async () => {
+ saga = new AsyncSaga(explorer);
+
+ saga.put(explorerActivateFile('test.file'));
+
+ await expect(saga.take()).resolves.toEqual(editorActivateFile('test.file'));
+ });
+
+ it('should propagate error', async () => {
+ const testError = new Error('test error');
+ saga.put(editorDidFailToActivateFile('test.file', testError));
+
+ await expect(saga.take()).resolves.toEqual(
+ explorerDidFailToActivateFile('test.file', testError),
+ );
+ });
+
+ it('should propagate success', async () => {
+ saga.put(editorDidActivateFile('test.file'));
+
+ await expect(saga.take()).resolves.toEqual(
+ explorerDidActivateFile('test.file'),
+ );
+ });
+
+ afterEach(async () => {
+ await saga.end();
+ });
+});
+
describe('handleExplorerRenameFile', () => {
let saga: AsyncSaga;
diff --git a/src/explorer/sagas.ts b/src/explorer/sagas.ts
index bff37894..08739299 100644
--- a/src/explorer/sagas.ts
+++ b/src/explorer/sagas.ts
@@ -12,6 +12,11 @@ import {
takeEvery,
takeLatest,
} from 'typed-redux-saga/macro';
+import {
+ editorActivateFile,
+ editorDidActivateFile,
+ editorDidFailToActivateFile,
+} from '../editor/actions';
import { getPybricksMicroPythonFileTemplate } from '../editor/pybricksMicroPython';
import {
fileStorageDidDumpAllFiles,
@@ -37,11 +42,14 @@ import {
import { RootState } from '../reducers';
import { defined, ensureError, timestamp } from '../utils';
import {
+ explorerActivateFile,
explorerArchiveAllFiles,
explorerCreateNewFile,
+ explorerDidActivateFile,
explorerDidArchiveAllFiles,
explorerDidCreateNewFile,
explorerDidExportFile,
+ explorerDidFailToActivateFile,
explorerDidFailToArchiveAllFiles,
explorerDidFailToCreateNewFile,
explorerDidFailToExportFile,
@@ -192,6 +200,39 @@ function* handleExplorerCreateNewFile(
}
}
+/**
+ * Connects user triggered action to editor module.
+ * @param action
+ */
+function* handleExplorerActivateFile(
+ action: ReturnType,
+): Generator {
+ yield* put(editorActivateFile(action.fileName));
+
+ const { didActivate, didFailToActivate } = yield* race({
+ didActivate: take(
+ editorDidActivateFile.when((a) => a.fileName === action.fileName),
+ ),
+ didFailToActivate: take(
+ editorDidFailToActivateFile.when((a) => a.fileName === action.fileName),
+ ),
+ });
+
+ if (didFailToActivate) {
+ yield* put(
+ explorerDidFailToActivateFile(
+ didFailToActivate.fileName,
+ didFailToActivate.error,
+ ),
+ );
+ return;
+ }
+
+ defined(didActivate);
+
+ yield* put(explorerDidActivateFile(didActivate.fileName));
+}
+
/** Connects user initiate rename file actions to the rename file dialog. */
function* handleExplorerRenameFile(
action: ReturnType,
@@ -273,6 +314,7 @@ export default function* (): Generator {
yield* takeEvery(explorerArchiveAllFiles, handleExplorerArchiveAllFiles);
yield* takeEvery(explorerImportFiles, handleExplorerImportFiles);
yield* takeEvery(explorerCreateNewFile, handleExplorerCreateNewFile);
+ yield* takeEvery(explorerActivateFile, handleExplorerActivateFile);
// takeLatest should ensure that if we trigger a new rename before the
// previous one is finished, the old one will be canceled. We don't expect
// this to happen in practice though.