mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
explorer: create new file wizard
This commit is contained in:
@@ -253,42 +253,57 @@ export const language = <monaco.languages.IMonarchLanguage>{
|
||||
},
|
||||
};
|
||||
|
||||
function template(hubName: string, devices: string[]): string {
|
||||
return `from pybricks.hubs import ${hubName}
|
||||
from pybricks.pupdevices import ${devices.join(', ')}
|
||||
/**
|
||||
* Creates a new template for the given parameters.
|
||||
*
|
||||
* @param hubClassName The hub class name, e.g. `"MoveHub"`.
|
||||
* @param deviceClassNames A list of device class names, e.g. `["Motor"]`.
|
||||
* @returns The template.
|
||||
*/
|
||||
function createTemplate(hubClassName: string, deviceClassNames: string[]): string {
|
||||
return `from pybricks.hubs import ${hubClassName}
|
||||
from pybricks.pupdevices import ${deviceClassNames.join(', ')}
|
||||
from pybricks.parameters import Button, Color, Direction, Port, Stop
|
||||
from pybricks.robotics import DriveBase
|
||||
from pybricks.tools import wait, StopWatch
|
||||
|
||||
hub = ${hubName}()
|
||||
hub = ${hubClassName}()
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
type HubLabel =
|
||||
| 'movehub'
|
||||
| 'cityhub'
|
||||
| 'technichub'
|
||||
| 'inventorhub'
|
||||
| 'primehub'
|
||||
| 'essentialhub';
|
||||
|
||||
const templateSnippets: Array<
|
||||
Required<
|
||||
Pick<monaco.languages.CompletionItem, 'label' | 'documentation' | 'insertText'>
|
||||
> & { label: string }
|
||||
> & { label: HubLabel }
|
||||
> = [
|
||||
{
|
||||
label: 'technichub',
|
||||
documentation: 'Template for Technic hub program.',
|
||||
insertText: template('TechnicHub', ['Motor']),
|
||||
insertText: createTemplate('TechnicHub', ['Motor']),
|
||||
},
|
||||
{
|
||||
label: 'cityhub',
|
||||
documentation: 'Template for City hub program.',
|
||||
insertText: template('CityHub', ['DCMotor', 'Light']),
|
||||
insertText: createTemplate('CityHub', ['DCMotor', 'Light']),
|
||||
},
|
||||
{
|
||||
label: 'movehub',
|
||||
documentation: 'Template for BOOST Move hub program.',
|
||||
insertText: template('MoveHub', ['Motor', 'ColorDistanceSensor']),
|
||||
insertText: createTemplate('MoveHub', ['Motor', 'ColorDistanceSensor']),
|
||||
},
|
||||
{
|
||||
label: 'inventorhub',
|
||||
documentation: 'Template for MINDSTORMS Robot Inventor hub program.',
|
||||
insertText: template('InventorHub', [
|
||||
insertText: createTemplate('InventorHub', [
|
||||
'Motor',
|
||||
'ColorSensor',
|
||||
'UltrasonicSensor',
|
||||
@@ -297,7 +312,7 @@ const templateSnippets: Array<
|
||||
{
|
||||
label: 'primehub',
|
||||
documentation: 'Template for SPIKE Prime program.',
|
||||
insertText: template('PrimeHub', [
|
||||
insertText: createTemplate('PrimeHub', [
|
||||
'Motor',
|
||||
'ColorSensor',
|
||||
'UltrasonicSensor',
|
||||
@@ -307,7 +322,7 @@ const templateSnippets: Array<
|
||||
{
|
||||
label: 'essentialhub',
|
||||
documentation: 'Template for SPIKE Essential program.',
|
||||
insertText: template('EssentialHub', [
|
||||
insertText: createTemplate('EssentialHub', [
|
||||
'Motor',
|
||||
'ColorSensor',
|
||||
'ColorLightMatrix',
|
||||
@@ -315,6 +330,14 @@ const templateSnippets: Array<
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the template text for a Pybricks MicroPython file.
|
||||
* @param hub The hub label.
|
||||
*/
|
||||
export function getPybricksMicroPythonFileTemplate(hub: HubLabel): string | undefined {
|
||||
return templateSnippets.find((t) => t.label === hub)?.insertText;
|
||||
}
|
||||
|
||||
export const templateSnippetCompletions = <monaco.languages.CompletionItemProvider>{
|
||||
provideCompletionItems: (model, position, _context, _token) => {
|
||||
// templates snippets are only available on the first line
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { getByLabelText, waitFor } from '@testing-library/dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { testRender } from '../../test';
|
||||
@@ -37,6 +38,22 @@ describe('archive button', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('new file button', () => {
|
||||
it('should show new file wizard', async () => {
|
||||
const [explorer] = testRender(<Explorer />);
|
||||
|
||||
const button = explorer.getByTitle('Create a new file');
|
||||
|
||||
userEvent.click(button);
|
||||
|
||||
const dialog = explorer.getByRole('dialog');
|
||||
expect(dialog).toBeVisible();
|
||||
|
||||
userEvent.click(getByLabelText(dialog, 'Close'));
|
||||
await waitFor(() => expect(dialog).not.toBeVisible());
|
||||
});
|
||||
});
|
||||
|
||||
describe('list item', () => {
|
||||
it('should show/hide buttons on hover', () => {
|
||||
const [explorer] = testRender(<Explorer />, {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
fileStorageExportFile,
|
||||
} from '../fileStorage/actions';
|
||||
import { useSelector } from '../reducers';
|
||||
import NewFileWizard from './NewFileWizard';
|
||||
import { ExplorerStringId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
|
||||
@@ -102,6 +103,7 @@ const FileActionButtonGroup = forwardRef<
|
||||
FileActionButtonGroup.displayName = 'FileActionButtonGroup';
|
||||
|
||||
const Header: React.VFC = () => {
|
||||
const [isNewFileWizardOpen, setIsNewFileWizardOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
|
||||
@@ -125,7 +127,11 @@ const Header: React.VFC = () => {
|
||||
<ActionButton
|
||||
icon="plus"
|
||||
toolTipId={ExplorerStringId.HeaderAddNewTooltip}
|
||||
onClick={() => alert('not implemented')}
|
||||
onClick={() => setIsNewFileWizardOpen(true)}
|
||||
/>
|
||||
<NewFileWizard
|
||||
isOpen={isNewFileWizardOpen}
|
||||
onClose={() => setIsNewFileWizardOpen(false)}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
Dialog,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Tag,
|
||||
} from '@blueprintjs/core';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useSelector } from '../reducers';
|
||||
import { FileExtension, Hub, explorerCreateNewFile } from './actions';
|
||||
import { NewFileWizardStringId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
|
||||
// This should be set to the most commonly used hub.
|
||||
const defaultHub = Hub.Technic;
|
||||
|
||||
/** File name validation results. */
|
||||
enum FileNameValidationResult {
|
||||
/** The file name is acceptable. */
|
||||
IsOk,
|
||||
/** The file name is an empty string. */
|
||||
IsEmpty,
|
||||
/** The file name contains spaces. */
|
||||
HasSpaces,
|
||||
/** The file name include the file file extension. */
|
||||
HasFileExtension,
|
||||
/** The first character is not a letter or underscore. */
|
||||
HasInvalidFirstCharacter,
|
||||
/** The file name has invalid characters. */
|
||||
HasInvalidCharacters,
|
||||
/** A file with the same name already exists. */
|
||||
AlreadyExists,
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the file name according to a number of criteria.
|
||||
*
|
||||
* @param fileName The file name (without extension).
|
||||
* @param extension The file extension (include ".").
|
||||
* @param existingFiles List of existing files.
|
||||
* @returns The result of the validation.
|
||||
*/
|
||||
function validateFileName(
|
||||
fileName: string,
|
||||
extension: string,
|
||||
existingFiles: ReadonlyArray<string>,
|
||||
): FileNameValidationResult {
|
||||
if (existingFiles.includes(`${fileName}${extension}`)) {
|
||||
return FileNameValidationResult.AlreadyExists;
|
||||
}
|
||||
|
||||
if (fileName.length === 0) {
|
||||
return FileNameValidationResult.IsEmpty;
|
||||
}
|
||||
|
||||
if (fileName.match(/\s/)) {
|
||||
return FileNameValidationResult.HasSpaces;
|
||||
}
|
||||
|
||||
if (fileName.endsWith(extension)) {
|
||||
return FileNameValidationResult.HasFileExtension;
|
||||
}
|
||||
|
||||
if (!fileName.match(/^[a-zA-Z_]/)) {
|
||||
return FileNameValidationResult.HasInvalidFirstCharacter;
|
||||
}
|
||||
|
||||
if (!fileName.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||
return FileNameValidationResult.HasInvalidCharacters;
|
||||
}
|
||||
|
||||
return FileNameValidationResult.IsOk;
|
||||
}
|
||||
|
||||
type FileNameHelpTextProps = {
|
||||
validation: FileNameValidationResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component that maps FileNameValidationResult to help message to display to user.
|
||||
*/
|
||||
const FileNameHelpText: React.VoidFunctionComponent<FileNameHelpTextProps> = (
|
||||
props,
|
||||
) => {
|
||||
const [i18n] = useI18n({ id: 'newFileWizard', translations: { en }, fallback: en });
|
||||
|
||||
switch (props.validation) {
|
||||
case FileNameValidationResult.IsOk:
|
||||
return <>{i18n.translate(NewFileWizardStringId.FileNameHelpTextIsOk)}</>;
|
||||
case FileNameValidationResult.IsEmpty:
|
||||
return <>{i18n.translate(NewFileWizardStringId.FileNameHelpTextIsEmpty)}</>;
|
||||
case FileNameValidationResult.HasSpaces:
|
||||
return (
|
||||
<>{i18n.translate(NewFileWizardStringId.FileNameHelpTextHasSpaces)}</>
|
||||
);
|
||||
case FileNameValidationResult.HasFileExtension:
|
||||
return (
|
||||
<>
|
||||
{i18n.translate(
|
||||
NewFileWizardStringId.FileNameHelpTextHasFileExtension,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
case FileNameValidationResult.HasInvalidFirstCharacter:
|
||||
return (
|
||||
<>
|
||||
{i18n.translate(
|
||||
NewFileWizardStringId.FileNameHelpTextHasInvalidFirstCharacter,
|
||||
{
|
||||
letters: <code className={Classes.CODE}>a…z</code>,
|
||||
underscore: <code className={Classes.CODE}>_</code>,
|
||||
},
|
||||
)}
|
||||
</>
|
||||
);
|
||||
case FileNameValidationResult.HasInvalidCharacters:
|
||||
return (
|
||||
<>
|
||||
{i18n.translate(
|
||||
NewFileWizardStringId.FileNameHelpTextHasInvalidCharacters,
|
||||
{
|
||||
letters: <code className={Classes.CODE}>a…z</code>,
|
||||
numbers: <code className={Classes.CODE}>0…9</code>,
|
||||
dash: <code className={Classes.CODE}>-</code>,
|
||||
underscore: <code className={Classes.CODE}>_</code>,
|
||||
},
|
||||
)}
|
||||
</>
|
||||
);
|
||||
case FileNameValidationResult.AlreadyExists:
|
||||
return (
|
||||
<>
|
||||
{i18n.translate(
|
||||
NewFileWizardStringId.FileNameHelpTextAlreadyExists,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
type NewFileWizardProps = {
|
||||
readonly isOpen: boolean;
|
||||
readonly onClose: () => void;
|
||||
};
|
||||
|
||||
const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) => {
|
||||
const [i18n] = useI18n({ id: 'newFileWizard', translations: { en }, fallback: en });
|
||||
const dispatch = useDispatch();
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [fileNameValidation, setFileNameValidation] = useState(
|
||||
FileNameValidationResult.IsEmpty,
|
||||
);
|
||||
const [hubType, setHubType] = useState(defaultHub);
|
||||
|
||||
const fileNameInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const fileNameIntent =
|
||||
fileNameValidation === FileNameValidationResult.IsOk ? 'none' : 'danger';
|
||||
|
||||
const handleFileNameChanged = (fileName: string) => {
|
||||
setFileNameValidation(
|
||||
validateFileName(fileName, FileExtension.Python, fileNames),
|
||||
);
|
||||
setFileName(fileName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
icon="plus"
|
||||
title={i18n.translate(NewFileWizardStringId.Title)}
|
||||
isOpen={props.isOpen}
|
||||
onOpening={() => handleFileNameChanged('')}
|
||||
onOpened={() => fileNameInputRef.current?.focus()}
|
||||
onClose={() => props.onClose()}
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<FormGroup
|
||||
label={i18n.translate(NewFileWizardStringId.FileNameLabel)}
|
||||
intent={fileNameIntent}
|
||||
subLabel={<FileNameHelpText validation={fileNameValidation} />}
|
||||
>
|
||||
<InputGroup
|
||||
aria-label="File name"
|
||||
value={fileName}
|
||||
inputRef={fileNameInputRef}
|
||||
intent={fileNameIntent}
|
||||
rightElement={<Tag>{FileExtension.Python}</Tag>}
|
||||
onChange={(e) => handleFileNameChanged(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={i18n.translate(NewFileWizardStringId.SmartHubLabel)}>
|
||||
<RadioGroup
|
||||
selectedValue={hubType}
|
||||
onChange={(e) => setHubType(e.currentTarget.value as Hub)}
|
||||
>
|
||||
<Radio value={Hub.Move}>BOOST Move Hub</Radio>
|
||||
<Radio value={Hub.City}>City Hub</Radio>
|
||||
<Radio value={Hub.Technic}>Technic Hub</Radio>
|
||||
<Radio value={Hub.Prime}>SPIKE Prime</Radio>
|
||||
<Radio value={Hub.Essential}>SPIKE Essential</Radio>
|
||||
<Radio value={Hub.Inventor}>MINDSTORMS Robot Inventor</Radio>
|
||||
</RadioGroup>
|
||||
</FormGroup>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button
|
||||
aria-label="Create"
|
||||
intent="primary"
|
||||
disabled={fileNameValidation !== FileNameValidationResult.IsOk}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
dispatch(
|
||||
explorerCreateNewFile(
|
||||
fileName,
|
||||
FileExtension.Python,
|
||||
hubType,
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n.translate(NewFileWizardStringId.ActionCreate)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewFileWizard;
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { testRender } from '../../test';
|
||||
import NewFileWizard from './NewFileWizard';
|
||||
|
||||
describe('create button', () => {
|
||||
it('should close the dialog', async () => {
|
||||
const onClose = jest.fn();
|
||||
const [dialog] = testRender(<NewFileWizard isOpen={true} onClose={onClose} />);
|
||||
|
||||
const button = dialog.getByLabelText('Create');
|
||||
|
||||
// have to type a file name before Create button is enabled
|
||||
userEvent.type(dialog.getByLabelText('File name'), 'test');
|
||||
await waitFor(() => expect(button).not.toBeDisabled());
|
||||
|
||||
userEvent.click(button);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { createAction } from '../actions';
|
||||
|
||||
/** Supported file extensions. */
|
||||
export enum FileExtension {
|
||||
/** Python (.py) */
|
||||
Python = '.py',
|
||||
}
|
||||
|
||||
/** 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',
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that requests to create a new file.
|
||||
* @param fileName The requested new file name (without file extension).
|
||||
* @param fileExtension The file extension (including leading ".").
|
||||
* @param hub The type of hub this file is for.
|
||||
*/
|
||||
export const explorerCreateNewFile = createAction(
|
||||
(fileName: string, fileExtension: FileExtension, hub: Hub) => ({
|
||||
type: 'explorer.action.createNewFile',
|
||||
fileName,
|
||||
fileExtension,
|
||||
hub,
|
||||
}),
|
||||
);
|
||||
@@ -10,5 +10,26 @@
|
||||
"exportTooltip": "Export {fileName}",
|
||||
"renameTooltip": "Rename {fileName}"
|
||||
}
|
||||
},
|
||||
"newFileWizard": {
|
||||
"title": "Create a new file",
|
||||
"fileName": {
|
||||
"label": "File name",
|
||||
"helpText": {
|
||||
"isOk": "OK!",
|
||||
"isEmpty": "File name cannot be empty.",
|
||||
"hasSpaces": "File name cannot have spaces.",
|
||||
"hasFileExtension": "The file extension will be added automatically.",
|
||||
"hasInvalidFirstCharacter": "File name must start with a letter ({letters}) or underscore ({underscore}).",
|
||||
"hasInvalidCharacters": "File name can only contain letters ({letters}), numbers ({numbers}), dashes ({dash}) and underscores ({underscore}).",
|
||||
"alreadyExists": "A file with this name already exists."
|
||||
}
|
||||
},
|
||||
"smartHub": {
|
||||
"label": "Smart hub"
|
||||
},
|
||||
"action": {
|
||||
"create": "Create"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { lookup } from '../../test';
|
||||
import { ExplorerStringId } from './i18n';
|
||||
import { ExplorerStringId, NewFileWizardStringId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
|
||||
describe('Ensure .json file has matches for ExplorerStringId', () => {
|
||||
@@ -10,3 +10,9 @@ describe('Ensure .json file has matches for ExplorerStringId', () => {
|
||||
expect(lookup(en, id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ensure .json file has matches for NewFileWizardStringId', () => {
|
||||
test.each(Object.values(NewFileWizardStringId))('%s', (id) => {
|
||||
expect(lookup(en, id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,3 +11,17 @@ export enum ExplorerStringId {
|
||||
TreeItemExportTooltip = 'explorer.treeItem.exportTooltip',
|
||||
TreeItemRenameTooltip = 'explorer.treeItem.renameTooltip',
|
||||
}
|
||||
|
||||
export enum NewFileWizardStringId {
|
||||
Title = 'newFileWizard.title',
|
||||
FileNameLabel = 'newFileWizard.fileName.label',
|
||||
FileNameHelpTextIsOk = 'newFileWizard.fileName.helpText.isOk',
|
||||
FileNameHelpTextIsEmpty = 'newFileWizard.fileName.helpText.isEmpty',
|
||||
FileNameHelpTextHasSpaces = 'newFileWizard.fileName.helpText.hasSpaces',
|
||||
FileNameHelpTextHasFileExtension = 'newFileWizard.fileName.helpText.hasFileExtension',
|
||||
FileNameHelpTextHasInvalidFirstCharacter = 'newFileWizard.fileName.helpText.hasInvalidFirstCharacter',
|
||||
FileNameHelpTextHasInvalidCharacters = 'newFileWizard.fileName.helpText.hasInvalidCharacters',
|
||||
FileNameHelpTextAlreadyExists = 'newFileWizard.fileName.helpText.alreadyExists',
|
||||
SmartHubLabel = 'newFileWizard.smartHub.label',
|
||||
ActionCreate = 'newFileWizard.action.create',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { AsyncSaga } from '../../test';
|
||||
import { FileExtension, Hub, explorerCreateNewFile } from './actions';
|
||||
import explorer from './sagas';
|
||||
|
||||
describe('handleExplorerCreateNewFile', () => {
|
||||
it('should dispatch fileStorage action', async () => {
|
||||
const saga = new AsyncSaga(explorer);
|
||||
|
||||
saga.put(explorerCreateNewFile('test', FileExtension.Python, Hub.Technic));
|
||||
|
||||
const action = await saga.take();
|
||||
expect(action).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fileContents": "from pybricks.hubs import TechnicHub
|
||||
from pybricks.pupdevices import Motor
|
||||
from pybricks.parameters import Button, Color, Direction, Port, Stop
|
||||
from pybricks.robotics import DriveBase
|
||||
from pybricks.tools import wait, StopWatch
|
||||
|
||||
hub = TechnicHub()
|
||||
|
||||
",
|
||||
"fileName": "test.py",
|
||||
"type": "fileStorage.action.writeFile",
|
||||
}
|
||||
`);
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { put, takeEvery } from 'typed-redux-saga/macro';
|
||||
import { getPybricksMicroPythonFileTemplate } from '../editor/pybricksMicroPython';
|
||||
import { fileStorageWriteFile } from '../fileStorage/actions';
|
||||
import { explorerCreateNewFile } from './actions';
|
||||
|
||||
function* handleExplorerCreateNewFile(
|
||||
action: ReturnType<typeof explorerCreateNewFile>,
|
||||
): Generator {
|
||||
const fileName = `${action.fileName}${action.fileExtension}`;
|
||||
|
||||
yield* put(
|
||||
fileStorageWriteFile(
|
||||
fileName,
|
||||
getPybricksMicroPythonFileTemplate(action.hub) || '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield* takeEvery(explorerCreateNewFile, handleExplorerCreateNewFile);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import blePybricksService from './ble-pybricks-service/sagas';
|
||||
import ble from './ble/sagas';
|
||||
import editor, { EditorSagaContext } from './editor/sagas';
|
||||
import errorLog from './error-log/sagas';
|
||||
import explorer from './explorer/sagas';
|
||||
import fileStorage from './fileStorage/sagas';
|
||||
import flashFirmware, { FirmwareSagaContext } from './firmware/sagas';
|
||||
import hub from './hub/sagas';
|
||||
@@ -30,6 +31,7 @@ export default function* (): Generator {
|
||||
lwp3BootloaderProtocol(),
|
||||
editor(),
|
||||
errorLog(),
|
||||
explorer(),
|
||||
flashFirmware(),
|
||||
hub(),
|
||||
licenses(),
|
||||
|
||||
Reference in New Issue
Block a user