mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 10:35:11 +00:00
explorer: move export from fileStorage
This commit is contained in:
@@ -6,13 +6,14 @@ import { cleanup } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { testRender, uuid } from '../../test';
|
||||
import {
|
||||
FileMetadata,
|
||||
fileStorageArchiveAllFiles,
|
||||
fileStorageExportFile,
|
||||
} from '../fileStorage/actions';
|
||||
import { FileMetadata, fileStorageArchiveAllFiles } from '../fileStorage/actions';
|
||||
import Explorer from './Explorer';
|
||||
import { explorerDeleteFile, explorerImportFiles, explorerRenameFile } from './actions';
|
||||
import {
|
||||
explorerDeleteFile,
|
||||
explorerExportFile,
|
||||
explorerImportFiles,
|
||||
explorerRenameFile,
|
||||
} from './actions';
|
||||
|
||||
afterEach(async () => {
|
||||
cleanup();
|
||||
@@ -153,7 +154,7 @@ describe('tree item', () => {
|
||||
|
||||
userEvent.click(button);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(fileStorageExportFile('test.file'));
|
||||
expect(dispatch).toHaveBeenCalledWith(explorerExportFile('test.file'));
|
||||
});
|
||||
|
||||
it('should dispatch export action when key is pressed', async () => {
|
||||
@@ -166,6 +167,6 @@ describe('tree item', () => {
|
||||
userEvent.click(treeItem);
|
||||
userEvent.keyboard('{ctrl}e');
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(fileStorageExportFile('test.file'));
|
||||
expect(dispatch).toHaveBeenCalledWith(explorerExportFile('test.file'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,15 +23,17 @@ import {
|
||||
useTreeEnvironment,
|
||||
} from 'react-complex-tree';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
fileStorageArchiveAllFiles,
|
||||
fileStorageExportFile,
|
||||
} from '../fileStorage/actions';
|
||||
import { fileStorageArchiveAllFiles } from '../fileStorage/actions';
|
||||
import { useSelector } from '../reducers';
|
||||
import { isMacOS } from '../utils/os';
|
||||
import { preventBrowserNativeContextMenu } from '../utils/react';
|
||||
import { TreeItemContext, TreeItemData, renderers } from '../utils/tree-renderer';
|
||||
import { explorerDeleteFile, explorerImportFiles, explorerRenameFile } from './actions';
|
||||
import {
|
||||
explorerDeleteFile,
|
||||
explorerExportFile,
|
||||
explorerImportFiles,
|
||||
explorerRenameFile,
|
||||
} from './actions';
|
||||
import { I18nId } from './i18n';
|
||||
import NewFileWizard from './newFileWizard/NewFileWizard';
|
||||
import RenameFileDialog from './renameFileDialog/RenameFileDialog';
|
||||
@@ -110,7 +112,7 @@ const FileActionButtonGroup: React.VoidFunctionComponent<ActionButtonGroupProps>
|
||||
icon="import"
|
||||
tooltip={i18n.translate(I18nId.TreeItemExportTooltip, { fileName })}
|
||||
focusable={false}
|
||||
onClick={() => dispatch(fileStorageExportFile(fileName))}
|
||||
onClick={() => dispatch(explorerExportFile(fileName))}
|
||||
/>
|
||||
<ActionButton
|
||||
icon="trash"
|
||||
@@ -236,7 +238,7 @@ const renderTreeContainer: typeof renderers.renderTreeContainer = (props) => {
|
||||
const handleExportKeyDown = useCallback(() => {
|
||||
if (focusedItem !== undefined) {
|
||||
const fileName = environment.getItemTitle(environment.items[focusedItem]);
|
||||
dispatch(fileStorageExportFile(fileName));
|
||||
dispatch(explorerExportFile(fileName));
|
||||
}
|
||||
}, [environment]);
|
||||
|
||||
|
||||
@@ -86,6 +86,37 @@ export const explorerRenameFile = createAction((fileName: string) => ({
|
||||
fileName,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Request to export (download) a file.
|
||||
* @param fileName The file name.
|
||||
*/
|
||||
export const explorerExportFile = createAction((fileName: string) => ({
|
||||
type: 'explorer.action.exportFile',
|
||||
fileName,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Indicates that explorerExportFile(fileName) succeeded.
|
||||
* @param fileName The file name.
|
||||
*/
|
||||
export const explorerDidExportFile = createAction((fileName: string) => ({
|
||||
type: 'explorer.action.didExportFile',
|
||||
fileName,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Indicates that explorerExportFile(fileName) failed.
|
||||
* @param fileName The file name.
|
||||
* @param error The error that was raised.
|
||||
*/
|
||||
export const explorerDidFailToExportFile = createAction(
|
||||
(fileName: string, error: Error) => ({
|
||||
type: 'explorer.action.didFailToExportFile',
|
||||
fileName,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Action that indicates that {@link explorerRenameFile} succeeded.
|
||||
*/
|
||||
|
||||
@@ -6,11 +6,15 @@ import { FileWithHandle } from 'browser-fs-access';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { AsyncSaga, uuid } from '../../test';
|
||||
import {
|
||||
fileStorageDidFailToOpenFile,
|
||||
fileStorageDidFailToReadFile,
|
||||
fileStorageDidFailToRenameFile,
|
||||
fileStorageDidOpenFile,
|
||||
fileStorageDidReadFile,
|
||||
fileStorageDidRenameFile,
|
||||
fileStorageDidWriteFile,
|
||||
fileStorageOpenFile,
|
||||
fileStorageReadFile,
|
||||
fileStorageRenameFile,
|
||||
fileStorageWriteFile,
|
||||
} from '../fileStorage/actions';
|
||||
@@ -19,10 +23,13 @@ import {
|
||||
Hub,
|
||||
explorerCreateNewFile,
|
||||
explorerDidCreateNewFile,
|
||||
explorerDidExportFile,
|
||||
explorerDidFailToExportFile,
|
||||
explorerDidFailToImportFiles,
|
||||
explorerDidFailToRenameFile,
|
||||
explorerDidImportFiles,
|
||||
explorerDidRenameFile,
|
||||
explorerExportFile,
|
||||
explorerImportFiles,
|
||||
explorerRenameFile,
|
||||
} from './actions';
|
||||
@@ -159,3 +166,70 @@ describe('handleExplorerRenameFile', () => {
|
||||
await saga.end();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleExplorerExportFile', () => {
|
||||
let saga: AsyncSaga;
|
||||
const testFile = 'test.file';
|
||||
const testFileId = uuid(0);
|
||||
const testFileContents = '# test file contents';
|
||||
const testError = new Error('test error');
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(explorer);
|
||||
|
||||
saga.put(explorerExportFile(testFile));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageOpenFile(testFile));
|
||||
});
|
||||
|
||||
it('should fail if file does not exist', async () => {
|
||||
saga.put(fileStorageDidFailToOpenFile(testFile, testError));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
explorerDidFailToExportFile(testFile, testError),
|
||||
);
|
||||
});
|
||||
|
||||
describe('should read file', () => {
|
||||
beforeEach(async () => {
|
||||
saga.put(fileStorageDidOpenFile(testFile, testFileId));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageReadFile(testFileId));
|
||||
});
|
||||
|
||||
it('should catch read error', async () => {
|
||||
saga.put(fileStorageDidFailToReadFile(testFileId, testError));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
explorerDidFailToExportFile(testFile, testError),
|
||||
);
|
||||
});
|
||||
|
||||
describe('should read file', () => {
|
||||
it('should export file', async () => {
|
||||
jest.spyOn(browserFsAccess, 'fileSave').mockResolvedValue(null);
|
||||
|
||||
saga.put(fileStorageDidReadFile(testFileId, testFileContents));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
explorerDidExportFile('test.file'),
|
||||
);
|
||||
expect(browserFsAccess.fileSave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should catch error', async () => {
|
||||
jest.spyOn(browserFsAccess, 'fileSave').mockRejectedValue(testError);
|
||||
|
||||
saga.put(fileStorageDidReadFile(testFileId, testFileContents));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
explorerDidFailToExportFile('test.file', testError),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await saga.end();
|
||||
});
|
||||
});
|
||||
|
||||
+63
-1
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { fileOpen } from 'browser-fs-access';
|
||||
import { fileOpen, fileSave } from 'browser-fs-access';
|
||||
import {
|
||||
call,
|
||||
put,
|
||||
@@ -14,12 +14,15 @@ import {
|
||||
import { getPybricksMicroPythonFileTemplate } from '../editor/pybricksMicroPython';
|
||||
import {
|
||||
fileStorageDidFailToOpenFile,
|
||||
fileStorageDidFailToReadFile,
|
||||
fileStorageDidFailToRenameFile,
|
||||
fileStorageDidFailToWriteFile,
|
||||
fileStorageDidOpenFile,
|
||||
fileStorageDidReadFile,
|
||||
fileStorageDidRenameFile,
|
||||
fileStorageDidWriteFile,
|
||||
fileStorageOpenFile,
|
||||
fileStorageReadFile,
|
||||
fileStorageRenameFile,
|
||||
fileStorageWriteFile,
|
||||
} from '../fileStorage/actions';
|
||||
@@ -35,11 +38,14 @@ import { defined, ensureError } from '../utils';
|
||||
import {
|
||||
explorerCreateNewFile,
|
||||
explorerDidCreateNewFile,
|
||||
explorerDidExportFile,
|
||||
explorerDidFailToCreateNewFile,
|
||||
explorerDidFailToExportFile,
|
||||
explorerDidFailToImportFiles,
|
||||
explorerDidFailToRenameFile,
|
||||
explorerDidImportFiles,
|
||||
explorerDidRenameFile,
|
||||
explorerExportFile,
|
||||
explorerImportFiles,
|
||||
explorerRenameFile,
|
||||
} from './actions';
|
||||
@@ -207,6 +213,61 @@ function* handleExplorerRenameFile(
|
||||
yield* put(explorerDidRenameFile());
|
||||
}
|
||||
|
||||
function* handleExplorerExportFile(
|
||||
action: ReturnType<typeof explorerExportFile>,
|
||||
): Generator {
|
||||
try {
|
||||
yield* put(fileStorageOpenFile(action.fileName));
|
||||
|
||||
const { didOpen, didFailToOpen } = yield* race({
|
||||
didOpen: take(
|
||||
fileStorageDidOpenFile.when((a) => a.path === action.fileName),
|
||||
),
|
||||
didFailToOpen: take(
|
||||
fileStorageDidFailToOpenFile.when((a) => a.path === action.fileName),
|
||||
),
|
||||
});
|
||||
|
||||
if (didFailToOpen) {
|
||||
throw didFailToOpen.error;
|
||||
}
|
||||
|
||||
defined(didOpen);
|
||||
|
||||
yield* put(fileStorageReadFile(didOpen.id));
|
||||
|
||||
const { didRead, didFailToRead } = yield* race({
|
||||
didRead: take(fileStorageDidReadFile.when((a) => a.id === didOpen.id)),
|
||||
didFailToRead: take(
|
||||
fileStorageDidFailToReadFile.when((a) => a.id === didOpen.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (didFailToRead) {
|
||||
throw didFailToRead.error;
|
||||
}
|
||||
|
||||
defined(didRead);
|
||||
|
||||
const blob = new Blob([didRead.contents], { type: `${pythonFileMimeType}` });
|
||||
|
||||
yield* call(() =>
|
||||
fileSave(blob, {
|
||||
id: 'pybricksCodeFileStorageExport',
|
||||
fileName: didOpen.path,
|
||||
extensions: [pythonFileExtension],
|
||||
mimeTypes: [pythonFileMimeType],
|
||||
// TODO: translate description
|
||||
description: 'Python Files',
|
||||
}),
|
||||
);
|
||||
|
||||
yield* put(explorerDidExportFile(action.fileName));
|
||||
} catch (err) {
|
||||
yield* put(explorerDidFailToExportFile(action.fileName, ensureError(err)));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield* takeEvery(explorerImportFiles, handleExplorerImportFiles);
|
||||
yield* takeEvery(explorerCreateNewFile, handleExplorerCreateNewFile);
|
||||
@@ -214,4 +275,5 @@ export default function* (): Generator {
|
||||
// previous one is finished, the old one will be canceled. We don't expect
|
||||
// this to happen in practice though.
|
||||
yield* takeLatest(explorerRenameFile, handleExplorerRenameFile);
|
||||
yield* takeLatest(explorerExportFile, handleExplorerExportFile);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user