mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
add open and save button
Chrome has an annoying warning when downloading .py files, e.g https://stackoverflow.com/q/34130774/1976323 Workaround is to enable the "Ask where to save each file before downloading" setting.
This commit is contained in:
committed by
David Lechner
parent
ac6c798148
commit
1898787eb5
@@ -7,6 +7,7 @@
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^10.0.4",
|
||||
"@testing-library/user-event": "^10.3.4",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/jszip": "^3.1.7",
|
||||
"@types/node": "^12.0.0",
|
||||
@@ -18,6 +19,7 @@
|
||||
"@types/web-bluetooth": "^0.0.6",
|
||||
"ace-builds": "^1.4.9",
|
||||
"bootstrap": "^4.5.0",
|
||||
"file-saver": "^2.0.2",
|
||||
"jszip": "^3.4.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"react": "^16.13.1",
|
||||
|
||||
@@ -6,6 +6,14 @@ export enum EditorActionType {
|
||||
* The current (active) editor changed.
|
||||
*/
|
||||
Current = 'editor.action.current',
|
||||
/**
|
||||
* Save the current file to disk.
|
||||
*/
|
||||
Save = 'editor.action.save',
|
||||
/**
|
||||
* Open a file.
|
||||
*/
|
||||
Open = 'editor.action.open',
|
||||
}
|
||||
|
||||
export interface CurrentEditorAction extends Action<EditorActionType.Current> {
|
||||
@@ -21,3 +29,31 @@ export function setEditSession(
|
||||
): CurrentEditorAction {
|
||||
return { type: EditorActionType.Current, editSession };
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that saves the current file.
|
||||
*/
|
||||
export type EditorSaveAction = Action<EditorActionType.Save>;
|
||||
|
||||
/**
|
||||
* Creates an action to save the current file
|
||||
*/
|
||||
export function save(): EditorSaveAction {
|
||||
return { type: EditorActionType.Save };
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that opens a file.
|
||||
*/
|
||||
export interface EditorOpenAction extends Action<EditorActionType.Open> {
|
||||
/** The data to save */
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action to save a file
|
||||
* @param data The file data
|
||||
*/
|
||||
export function open(data: ArrayBuffer): EditorOpenAction {
|
||||
return { type: EditorActionType.Open, data };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import * as editor from '../actions/editor';
|
||||
import * as notification from '../actions/notification';
|
||||
import { RootState } from '../reducers';
|
||||
import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton';
|
||||
|
||||
type StateProps = Pick<OpenFileButtonProps, 'enabled'>;
|
||||
type DispatchProps = Pick<OpenFileButtonProps, 'onFile' | 'onReject'>;
|
||||
type OwnProps = Pick<OpenFileButtonProps, 'id'>;
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
enabled: state.editor.current !== null,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onFile: (data): void => {
|
||||
dispatch(editor.open(data));
|
||||
},
|
||||
onReject: (file): void => {
|
||||
dispatch(
|
||||
notification.add('error', `'${file.name}' is not a valid python file.`),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const mergeProps = (
|
||||
stateProps: StateProps,
|
||||
dispatchProps: DispatchProps,
|
||||
ownProps: OwnProps,
|
||||
): OpenFileButtonProps => ({
|
||||
fileExtension: '.py',
|
||||
tooltip: 'Load file',
|
||||
icon: 'open.svg',
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(OpenFileButton);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import * as editor from '../actions/editor';
|
||||
import { RootState } from '../reducers';
|
||||
import ActionButton, { ActionButtonProps } from './ActionButton';
|
||||
|
||||
type StateProps = Pick<ActionButtonProps, 'enabled' | 'context'>;
|
||||
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
|
||||
type OwnProps = Pick<ActionButtonProps, 'id'>;
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
enabled: state.editor.current !== null,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onAction: (): void => {
|
||||
dispatch(editor.save());
|
||||
},
|
||||
});
|
||||
|
||||
const mergeProps = (
|
||||
stateProps: StateProps,
|
||||
dispatchProps: DispatchProps,
|
||||
ownProps: OwnProps,
|
||||
): ActionButtonProps => ({
|
||||
tooltip: 'Save file',
|
||||
icon: 'download.svg',
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(ActionButton);
|
||||
@@ -3,14 +3,20 @@ import ButtonGroup from 'react-bootstrap/ButtonGroup';
|
||||
import ButtonToolbar from 'react-bootstrap/ButtonToolbar';
|
||||
import BluetoothButton from './BluetoothButton';
|
||||
import FlashButton from './FlashButton';
|
||||
import LoadButton from './LoadButton';
|
||||
import ReplButton from './ReplButton';
|
||||
import RunButton from './RunButton';
|
||||
import SaveButton from './SaveButton';
|
||||
import StopButton from './StopButton';
|
||||
|
||||
class Toolbar extends React.Component {
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<ButtonToolbar className="m-2">
|
||||
<ButtonGroup className="mr-2" size="lg">
|
||||
<LoadButton id="load" />
|
||||
<SaveButton id="save" />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="mr-2" size="lg">
|
||||
<BluetoothButton id="bluetooth" />
|
||||
<RunButton id="run" />
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as FileSaver from 'file-saver';
|
||||
import { Action, Dispatch } from 'redux';
|
||||
import { EditorActionType, EditorOpenAction } from '../actions/editor';
|
||||
import { RootState } from '../reducers';
|
||||
import { combineServices } from '.';
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
async function open(
|
||||
action: Action,
|
||||
_dispatch: Dispatch,
|
||||
state: RootState,
|
||||
): Promise<void> {
|
||||
if (action.type !== EditorActionType.Open) {
|
||||
return;
|
||||
}
|
||||
// istanbul ignore next: currently, it is a bug if there is no current editor
|
||||
if (state.editor.current === null) {
|
||||
console.error('No current editor');
|
||||
return;
|
||||
}
|
||||
const text = decoder.decode((action as EditorOpenAction).data);
|
||||
state.editor.current.getDocument().setValue(text);
|
||||
}
|
||||
|
||||
async function save(
|
||||
action: Action,
|
||||
_dispatch: Dispatch,
|
||||
state: RootState,
|
||||
): Promise<void> {
|
||||
if (action.type !== EditorActionType.Save) {
|
||||
return;
|
||||
}
|
||||
// istanbul ignore next: currently, it is a bug if there is no current editor
|
||||
if (state.editor.current === null) {
|
||||
console.error('No current editor');
|
||||
return;
|
||||
}
|
||||
const data = state.editor.current.getDocument().getValue();
|
||||
const blob = new Blob([data], { type: 'text/x-python;charset=utf-8' });
|
||||
FileSaver.saveAs(blob, 'main.py');
|
||||
}
|
||||
|
||||
export default combineServices(open, save);
|
||||
+14
-7
@@ -1,25 +1,32 @@
|
||||
import { Action, Dispatch, Middleware } from 'redux';
|
||||
import { RootState } from '../reducers';
|
||||
import bootloader from './bootloader';
|
||||
import editor from './editor';
|
||||
|
||||
type Service = (action: Action, dispatch: Dispatch) => Promise<void>;
|
||||
type Service = (action: Action, dispatch: Dispatch, state: RootState) => Promise<void>;
|
||||
|
||||
function runService(service: Service, action: Action, dispatch: Dispatch): void {
|
||||
service(action, dispatch).catch((err) =>
|
||||
function runService(
|
||||
service: Service,
|
||||
action: Action,
|
||||
dispatch: Dispatch,
|
||||
state: RootState,
|
||||
): void {
|
||||
service(action, dispatch, state).catch((err) =>
|
||||
console.log(`Unhandled exception in service: ${err}`),
|
||||
);
|
||||
}
|
||||
|
||||
export function combineServices(...services: Service[]): Service {
|
||||
return (a, d): Promise<void> => {
|
||||
services.forEach((s) => runService(s, a, d));
|
||||
return (a, d, s): Promise<void> => {
|
||||
services.forEach((x) => runService(x, a, d, s));
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
const rootService = combineServices(bootloader);
|
||||
const rootService = combineServices(bootloader, editor);
|
||||
|
||||
const serviceMiddleware: Middleware = (store) => (next) => (action): unknown => {
|
||||
runService(rootService, action, store.dispatch);
|
||||
runService(rootService, action, store.dispatch, store.getState());
|
||||
return next(action);
|
||||
};
|
||||
|
||||
|
||||
@@ -1508,6 +1508,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
|
||||
|
||||
"@types/file-saver@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.1.tgz#e18eb8b069e442f7b956d313f4fadd3ef887354e"
|
||||
integrity sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw==
|
||||
|
||||
"@types/glob@^7.1.1":
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
|
||||
@@ -4715,6 +4720,11 @@ file-loader@4.3.0:
|
||||
loader-utils "^1.2.3"
|
||||
schema-utils "^2.5.0"
|
||||
|
||||
file-saver@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.2.tgz#06d6e728a9ea2df2cce2f8d9e84dfcdc338ec17a"
|
||||
integrity sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==
|
||||
|
||||
file-selector@^0.1.12:
|
||||
version "0.1.12"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0"
|
||||
|
||||
Reference in New Issue
Block a user