mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
explorer: push file name validation to parents
This gets rid of the weird unknown state and other problems of trying to push state from a child back to a parent.
This commit is contained in:
@@ -3,15 +3,14 @@
|
||||
|
||||
import { Classes, FormGroup, InputGroup, Intent, Tag } from '@blueprintjs/core';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FileNameValidationResult, validateFileName } from '../pybricksMicropython/lib';
|
||||
import { useSelector } from '../reducers';
|
||||
import React from 'react';
|
||||
import { FileNameValidationResult } from '../pybricksMicropython/lib';
|
||||
import { NewFileWizardStringId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
|
||||
type FileNameHelpTextProps = {
|
||||
/** The result of the file name validation. */
|
||||
validation: Exclude<FileNameValidationResult, FileNameValidationResult.Unknown>;
|
||||
validation: FileNameValidationResult;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -81,12 +80,12 @@ type FileNameFormGroupProps = {
|
||||
readonly fileName: string;
|
||||
/** The file extension (including leading ".") */
|
||||
readonly fileExtension: string;
|
||||
/** The result of the file name validation. */
|
||||
readonly validationResult: FileNameValidationResult;
|
||||
/** Ref to get handle to input (e.g to be able to call focus()) */
|
||||
readonly inputRef?: React.RefObject<HTMLInputElement>;
|
||||
/** Called when the user changes the text in the input box. */
|
||||
readonly onChange: (newName: string) => void;
|
||||
/** Called when `fileName` is validated. */
|
||||
readonly onValidation: (result: FileNameValidationResult) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -95,30 +94,22 @@ type FileNameFormGroupProps = {
|
||||
const FileNameFormGroup: React.VoidFunctionComponent<FileNameFormGroupProps> = ({
|
||||
fileName,
|
||||
fileExtension,
|
||||
validationResult,
|
||||
inputRef,
|
||||
onChange,
|
||||
onValidation,
|
||||
}) => {
|
||||
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
|
||||
const [fileNameValidation, fileNameIntent] = useMemo(() => {
|
||||
const result = validateFileName(fileName, fileExtension, fileNames);
|
||||
|
||||
// can't call callback now because it would break react, so defer it
|
||||
setTimeout(() => onValidation(result), 0);
|
||||
|
||||
return [
|
||||
result,
|
||||
result === FileNameValidationResult.IsOk ? Intent.NONE : Intent.DANGER,
|
||||
];
|
||||
}, [fileName, fileExtension, fileNames]);
|
||||
const fileNameIntent =
|
||||
validationResult === FileNameValidationResult.IsOk
|
||||
? Intent.NONE
|
||||
: Intent.DANGER;
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={i18n.translate(NewFileWizardStringId.FileNameLabel)}
|
||||
intent={fileNameIntent}
|
||||
subLabel={<FileNameHelpText validation={fileNameValidation} />}
|
||||
subLabel={<FileNameHelpText validation={validationResult} />}
|
||||
>
|
||||
<InputGroup
|
||||
aria-label="File name"
|
||||
|
||||
@@ -15,7 +15,9 @@ import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
FileNameValidationResult,
|
||||
pythonFileExtension,
|
||||
validateFileName,
|
||||
} from '../pybricksMicropython/lib';
|
||||
import { useSelector } from '../reducers';
|
||||
import FileNameFormGroup from './FileNameFormGroup';
|
||||
import { Hub, explorerCreateNewFile } from './actions';
|
||||
import { NewFileWizardStringId } from './i18n';
|
||||
@@ -39,8 +41,11 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = ({
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [fileNameValidation, setFileNameValidation] = useState(
|
||||
FileNameValidationResult.Unknown,
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
const fileNameValidation = validateFileName(
|
||||
fileName,
|
||||
pythonFileExtension,
|
||||
fileNames,
|
||||
);
|
||||
const [hubType, setHubType] = useState(defaultHub);
|
||||
|
||||
@@ -59,9 +64,9 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = ({
|
||||
<FileNameFormGroup
|
||||
fileName={fileName}
|
||||
fileExtension={pythonFileExtension}
|
||||
validationResult={fileNameValidation}
|
||||
inputRef={fileNameInputRef}
|
||||
onChange={setFileName}
|
||||
onValidation={setFileNameValidation}
|
||||
/>
|
||||
<FormGroup label={i18n.translate(NewFileWizardStringId.SmartHubLabel)}>
|
||||
<RadioGroup
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import { Button, Classes, Dialog } from '@blueprintjs/core';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { FileNameValidationResult } from '../pybricksMicropython/lib';
|
||||
import { FileNameValidationResult, validateFileName } from '../pybricksMicropython/lib';
|
||||
import { useSelector } from '../reducers';
|
||||
import FileNameFormGroup from './FileNameFormGroup';
|
||||
import { RenameFileStringId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
@@ -31,7 +32,8 @@ const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = ({
|
||||
const [baseName, extension] = oldName.split(/(\.\w+)$/);
|
||||
|
||||
const [newName, setNewName] = useState(baseName);
|
||||
const [result, setResult] = useState(FileNameValidationResult.Unknown);
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
const result = validateFileName(newName, extension, fileNames);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -61,9 +63,9 @@ const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = ({
|
||||
<FileNameFormGroup
|
||||
fileName={newName}
|
||||
fileExtension={extension}
|
||||
validationResult={result}
|
||||
inputRef={inputRef}
|
||||
onChange={setNewName}
|
||||
onValidation={setResult}
|
||||
/>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
|
||||
@@ -12,8 +12,6 @@ export const pythonFileMimeType = 'text/x-python';
|
||||
|
||||
/** File name validation results. */
|
||||
export enum FileNameValidationResult {
|
||||
/** The result is not yet known. */
|
||||
Unknown,
|
||||
/** The file name is acceptable. */
|
||||
IsOk,
|
||||
/** The file name is an empty string. */
|
||||
@@ -42,7 +40,7 @@ export function validateFileName(
|
||||
fileName: string,
|
||||
extension: string,
|
||||
existingFiles: ReadonlyArray<string>,
|
||||
): Exclude<FileNameValidationResult, FileNameValidationResult.Unknown> {
|
||||
): FileNameValidationResult {
|
||||
if (existingFiles.includes(`${fileName}${extension}`)) {
|
||||
return FileNameValidationResult.AlreadyExists;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user