mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
fileStorage: new file storage backend
This replaces the localStorage file storage with a new backend that uses localForage (indexeddb) for storing the file contents. This will also allow storing multiple files.
This commit is contained in:
+5
-19
@@ -10,16 +10,17 @@ import {
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import tomorrowNightEightiesTheme from 'monaco-themes/themes/Tomorrow-Night-Eighties.json';
|
||||
import xcodeTheme from 'monaco-themes/themes/Xcode_default.json';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import MonacoEditor, { monaco } from 'react-monaco-editor';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { useDispatch } from '../actions';
|
||||
import { fileStorageWriteFile } from '../fileStorage/actions';
|
||||
import { compile } from '../mpy/actions';
|
||||
import { useSelector } from '../reducers';
|
||||
import { toggleBoolean } from '../settings/actions';
|
||||
import { BooleanSettingId } from '../settings/defaults';
|
||||
import { isMacOS } from '../utils/os';
|
||||
import { setEditSession, storageChanged } from './actions';
|
||||
import { setEditSession } from './actions';
|
||||
import { EditorStringId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
import * as pybricksMicroPython from './pybricksMicroPython';
|
||||
@@ -128,21 +129,6 @@ const Editor: React.FunctionComponent = (_props) => {
|
||||
const editorRef = useRef<MonacoEditor>(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onStorage = (e: StorageEvent): void => {
|
||||
if (
|
||||
e.key === 'program' &&
|
||||
e.newValue &&
|
||||
e.newValue !== editorRef.current?.editor?.getValue()
|
||||
) {
|
||||
dispatch(storageChanged(e.newValue));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('storage', onStorage);
|
||||
return () => window.removeEventListener('storage', onStorage);
|
||||
});
|
||||
|
||||
const darkMode = useSelector((s) => s.settings.darkMode);
|
||||
|
||||
const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en });
|
||||
@@ -166,7 +152,6 @@ const Editor: React.FunctionComponent = (_props) => {
|
||||
contextmenu: false,
|
||||
rulers: [80],
|
||||
}}
|
||||
value={localStorage.getItem('program')}
|
||||
editorDidMount={(editor, _monaco) => {
|
||||
const subscriptions = new Array<IDisposable>();
|
||||
// FIXME: editor does not respond to changes in i18n
|
||||
@@ -221,7 +206,8 @@ const Editor: React.FunctionComponent = (_props) => {
|
||||
editor.focus();
|
||||
dispatch(setEditSession(editor));
|
||||
}}
|
||||
onChange={(v) => localStorage.setItem('program', v)}
|
||||
// REVIST: need to ensure we have exclusive access to file
|
||||
onChange={(v) => dispatch(fileStorageWriteFile('main.py', v))}
|
||||
/>
|
||||
</ContextMenu2>
|
||||
</ResizeSensor2>
|
||||
|
||||
+1
-28
@@ -15,10 +15,6 @@ export enum EditorActionType {
|
||||
DidFailToSaveAs = 'editor.action.didFailToSaveAs',
|
||||
/** Open a file. */
|
||||
Open = 'editor.action.open',
|
||||
/** Storage was changed outside of the app. */
|
||||
StorageChanged = 'editor.action.storageChanged',
|
||||
/** Reload program from local storage. */
|
||||
ReloadProgram = 'editor.action.reloadProgram',
|
||||
}
|
||||
|
||||
export type CurrentEditorAction = Action<EditorActionType.Current> & {
|
||||
@@ -81,27 +77,6 @@ export function open(data: ArrayBuffer): EditorOpenAction {
|
||||
return { type: EditorActionType.Open, data };
|
||||
}
|
||||
|
||||
/**Action that indicates the local storage has changed. */
|
||||
export type EditorStorageChangedAction = Action<EditorActionType.StorageChanged> & {
|
||||
newValue: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an action that indicates the local storage has changed.
|
||||
* @param newValue The new program.
|
||||
*/
|
||||
export function storageChanged(newValue: string): EditorStorageChangedAction {
|
||||
return { type: EditorActionType.StorageChanged, newValue };
|
||||
}
|
||||
|
||||
/** Action to request reloading the program from local storage. */
|
||||
export type EditorReloadProgramAction = Action<EditorActionType.ReloadProgram>;
|
||||
|
||||
/** Creates and action to request reloading the program from local storage. */
|
||||
export function reloadProgram(): EditorReloadProgramAction {
|
||||
return { type: EditorActionType.ReloadProgram };
|
||||
}
|
||||
|
||||
/**
|
||||
* Common type for all editor actions.
|
||||
*/
|
||||
@@ -110,6 +85,4 @@ export type EditorAction =
|
||||
| EditorOpenAction
|
||||
| EditorSaveAsAction
|
||||
| EditorDidSaveAsAction
|
||||
| EditorDidFailToSaveAsAction
|
||||
| EditorStorageChangedAction
|
||||
| EditorReloadProgramAction;
|
||||
| EditorDidFailToSaveAsAction;
|
||||
|
||||
@@ -5,7 +5,7 @@ import FileSaver from 'file-saver';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { monaco } from 'react-monaco-editor';
|
||||
import { AsyncSaga } from '../../test';
|
||||
import { didFailToSaveAs, didSaveAs, open, reloadProgram, saveAs } from './actions';
|
||||
import { didFailToSaveAs, didSaveAs, open, saveAs } from './actions';
|
||||
import editor from './sagas';
|
||||
|
||||
jest.mock('react-monaco-editor');
|
||||
@@ -123,14 +123,3 @@ describe('saveAs', () => {
|
||||
mockFileSaverSaveAs.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
test('reloadProgram', async () => {
|
||||
const mockEditor = mock<monaco.editor.ICodeEditor>();
|
||||
const saga = new AsyncSaga(editor, { editor: { current: mockEditor } });
|
||||
|
||||
saga.put(reloadProgram());
|
||||
|
||||
expect(mockEditor.setValue).toHaveBeenCalled();
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
+54
-11
@@ -1,14 +1,29 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import { call, put, select, takeEvery } from 'typed-redux-saga/macro';
|
||||
import {
|
||||
call,
|
||||
put,
|
||||
race,
|
||||
select,
|
||||
take,
|
||||
takeEvery,
|
||||
takeLatest,
|
||||
} from 'typed-redux-saga/macro';
|
||||
import { Action } from '../actions';
|
||||
import {
|
||||
FileStorageActionType,
|
||||
FileStorageDidFailToReadFileAction,
|
||||
FileStorageDidReadFileAction,
|
||||
fileStorageReadFile,
|
||||
} from '../fileStorage/actions';
|
||||
import { RootState } from '../reducers';
|
||||
import { ensureError } from '../utils';
|
||||
import {
|
||||
CurrentEditorAction,
|
||||
EditorActionType,
|
||||
EditorOpenAction,
|
||||
EditorReloadProgramAction,
|
||||
EditorSaveAsAction,
|
||||
didFailToSaveAs,
|
||||
didSaveAs,
|
||||
@@ -78,20 +93,48 @@ function* saveAs(_action: EditorSaveAsAction): Generator {
|
||||
yield* put(didSaveAs());
|
||||
}
|
||||
|
||||
function* reloadProgram(_action: EditorReloadProgramAction): Generator {
|
||||
const editor = yield* select((s: RootState) => s.editor.current);
|
||||
|
||||
// istanbul ignore next: it is a bug to dispatch this action with no current editor
|
||||
if (editor === null) {
|
||||
console.error('reloadProgram: No current editor');
|
||||
function* handleEditSession(action: CurrentEditorAction): Generator {
|
||||
if (action.editSession === null) {
|
||||
// there is not current edit session, nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
editor.setValue(localStorage.getItem('program') || '');
|
||||
// ensure storage has been initialized
|
||||
|
||||
const isStorageInitialized = yield* select(
|
||||
(s: RootState) => s.fileStorage.isInitialized,
|
||||
);
|
||||
|
||||
if (!isStorageInitialized) {
|
||||
yield* take(FileStorageActionType.DidInitialize);
|
||||
}
|
||||
|
||||
// TODO: get current file from state
|
||||
const currentFileName = 'main.py';
|
||||
|
||||
// TODO: implement locking to ensure exclusive access to file
|
||||
|
||||
yield* put(fileStorageReadFile(currentFileName));
|
||||
const { result } = yield* race({
|
||||
result: take<FileStorageDidReadFileAction>(
|
||||
(a: Action) =>
|
||||
a.type === FileStorageActionType.DidReadFile &&
|
||||
a.fileName === currentFileName,
|
||||
),
|
||||
error: take<FileStorageDidFailToReadFileAction>(
|
||||
(a: Action) =>
|
||||
a.type === FileStorageActionType.DidFailToReadFile &&
|
||||
a.fileName === currentFileName,
|
||||
),
|
||||
});
|
||||
|
||||
if (result) {
|
||||
action.editSession?.setValue(result.fileContents);
|
||||
}
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield* takeEvery(EditorActionType.Open, open);
|
||||
yield* takeEvery(EditorActionType.SaveAs, saveAs);
|
||||
yield* takeEvery(EditorActionType.ReloadProgram, reloadProgram);
|
||||
yield* takeLatest(EditorActionType.Current, handleEditSession);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user