mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
pybricksMicropython/lib: add module
This creates a new module for dealing with stuff specific to Pybricks MicroPython.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import * as notificationActions from '../notifications/actions';
|
||||
import { pythonFileExtension } from '../pybricksMicropython/lib';
|
||||
import OpenFileButton, { OpenFileButtonProps } from '../toolbar/OpenFileButton';
|
||||
import { TooltipId } from '../toolbar/i18n';
|
||||
import { EditorContext } from './Editor';
|
||||
@@ -18,7 +19,7 @@ const OpenButton: React.FunctionComponent<OpenButtonProps> = (props) => {
|
||||
|
||||
return (
|
||||
<OpenFileButton
|
||||
fileExtension=".py"
|
||||
fileExtension={pythonFileExtension}
|
||||
tooltip={TooltipId.Open}
|
||||
icon={openIcon}
|
||||
enabled={editor !== null}
|
||||
|
||||
+3
-2
@@ -3,6 +3,7 @@
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import { call, getContext, put, takeEvery } from 'typed-redux-saga/macro';
|
||||
import { pythonFileExtension, pythonFileMimeType } from '../pybricksMicropython/lib';
|
||||
import { ensureError } from '../utils';
|
||||
import { EditorType } from './Editor';
|
||||
import { didFailToSaveAs, didSaveAs, open, saveAs } from './actions';
|
||||
@@ -37,7 +38,7 @@ function* handleSaveAs(): Generator {
|
||||
}
|
||||
|
||||
const data = editor.getValue();
|
||||
const blob = new Blob([data], { type: 'text/x-python;charset=utf-8' });
|
||||
const blob = new Blob([data], { type: `${pythonFileMimeType};charset=utf-8` });
|
||||
|
||||
if (window.showSaveFilePicker) {
|
||||
// This uses https://wicg.github.io/file-system-access which is not
|
||||
@@ -48,7 +49,7 @@ function* handleSaveAs(): Generator {
|
||||
suggestedName: 'main.py',
|
||||
types: [
|
||||
{
|
||||
accept: { 'text/x-python': '.py' },
|
||||
accept: { [pythonFileMimeType]: pythonFileExtension },
|
||||
// TODO: translate description
|
||||
description: 'Python Files',
|
||||
},
|
||||
|
||||
@@ -14,72 +14,19 @@ import {
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
FileNameValidationResult,
|
||||
pythonFileExtension,
|
||||
validateFileName,
|
||||
} from '../pybricksMicropython/lib';
|
||||
import { useSelector } from '../reducers';
|
||||
import { FileExtension, Hub, explorerCreateNewFile } from './actions';
|
||||
import { 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;
|
||||
};
|
||||
@@ -169,7 +116,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) =
|
||||
|
||||
const handleFileNameChanged = (fileName: string) => {
|
||||
setFileNameValidation(
|
||||
validateFileName(fileName, FileExtension.Python, fileNames),
|
||||
validateFileName(fileName, pythonFileExtension, fileNames),
|
||||
);
|
||||
setFileName(fileName);
|
||||
};
|
||||
@@ -194,7 +141,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) =
|
||||
value={fileName}
|
||||
inputRef={fileNameInputRef}
|
||||
intent={fileNameIntent}
|
||||
rightElement={<Tag>{FileExtension.Python}</Tag>}
|
||||
rightElement={<Tag>{pythonFileExtension}</Tag>}
|
||||
onChange={(e) => handleFileNameChanged(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -224,7 +171,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) =
|
||||
dispatch(
|
||||
explorerCreateNewFile(
|
||||
fileName,
|
||||
FileExtension.Python,
|
||||
pythonFileExtension,
|
||||
hubType,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { createAction } from '../actions';
|
||||
import { pythonFileExtension } from '../pybricksMicropython/lib';
|
||||
|
||||
/** Supported file extensions. */
|
||||
export enum FileExtension {
|
||||
/** Python (.py) */
|
||||
Python = '.py',
|
||||
}
|
||||
type SupportedFileExtension = typeof pythonFileExtension;
|
||||
|
||||
/** Supported hub types. */
|
||||
export enum Hub {
|
||||
@@ -32,7 +30,7 @@ export enum Hub {
|
||||
* @param hub The type of hub this file is for.
|
||||
*/
|
||||
export const explorerCreateNewFile = createAction(
|
||||
(fileName: string, fileExtension: FileExtension, hub: Hub) => ({
|
||||
(fileName: string, fileExtension: SupportedFileExtension, hub: Hub) => ({
|
||||
type: 'explorer.action.createNewFile',
|
||||
fileName,
|
||||
fileExtension,
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { AsyncSaga } from '../../test';
|
||||
import { FileExtension, Hub, explorerCreateNewFile } from './actions';
|
||||
import { pythonFileExtension } from '../pybricksMicropython/lib';
|
||||
import { 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));
|
||||
saga.put(explorerCreateNewFile('test', pythonFileExtension, Hub.Technic));
|
||||
|
||||
const action = await saga.take();
|
||||
expect(action).toMatchInlineSnapshot(`
|
||||
|
||||
@@ -9,6 +9,7 @@ import { eventChannel } from 'redux-saga';
|
||||
import { call, fork, getContext, put, takeEvery } from 'typed-redux-saga/macro';
|
||||
import Observable from 'zen-observable';
|
||||
import { EditorType } from '../editor/Editor';
|
||||
import { pythonFileExtension, pythonFileMimeType } from '../pybricksMicropython/lib';
|
||||
import { ensureError, timestamp } from '../utils';
|
||||
import {
|
||||
fileStorageArchiveAllFiles,
|
||||
@@ -117,7 +118,7 @@ function* handleExportFile(
|
||||
suggestedName: action.fileName,
|
||||
types: [
|
||||
{
|
||||
accept: { 'text/x-python': '.py' },
|
||||
accept: { [pythonFileMimeType]: pythonFileExtension },
|
||||
// TODO: translate description
|
||||
description: 'Python Files',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { FileNameValidationResult, pythonFileExtension, validateFileName } from './lib';
|
||||
|
||||
describe('validateFileName', () => {
|
||||
it('should allow file names with underscores', () => {
|
||||
expect(validateFileName('file_name', pythonFileExtension, [])).toBe(
|
||||
FileNameValidationResult.IsOk,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow file names with dashes', () => {
|
||||
expect(validateFileName('file-name', pythonFileExtension, [])).toBe(
|
||||
FileNameValidationResult.IsOk,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow empty strings', () => {
|
||||
expect(validateFileName('', pythonFileExtension, [])).toBe(
|
||||
FileNameValidationResult.IsEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow file names with spaces', () => {
|
||||
expect(validateFileName('file name', pythonFileExtension, [])).toBe(
|
||||
FileNameValidationResult.HasSpaces,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow file names that start with numbers', () => {
|
||||
expect(validateFileName('1test', pythonFileExtension, [])).toBe(
|
||||
FileNameValidationResult.HasInvalidFirstCharacter,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow file names with symbols', () => {
|
||||
expect(validateFileName('test$', pythonFileExtension, [])).toBe(
|
||||
FileNameValidationResult.HasInvalidCharacters,
|
||||
);
|
||||
});
|
||||
|
||||
it('it should not allow file names that include the file extension', () => {
|
||||
expect(validateFileName('test.py', pythonFileExtension, [])).toBe(
|
||||
FileNameValidationResult.HasFileExtension,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow file names that match existing files', () => {
|
||||
expect(validateFileName('test', pythonFileExtension, ['test.py'])).toBe(
|
||||
FileNameValidationResult.AlreadyExists,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
/** The Python file extension ('.py') */
|
||||
export const pythonFileExtension = '.py';
|
||||
|
||||
/** The Python file MIME type ('text/x-python') */
|
||||
export const pythonFileMimeType = 'text/x-python';
|
||||
|
||||
/** File name validation results. */
|
||||
export 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 (including ".").
|
||||
* @param existingFiles List of existing files.
|
||||
* @returns The result of the validation.
|
||||
*/
|
||||
export 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;
|
||||
}
|
||||
Reference in New Issue
Block a user