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:
David Lechner
2022-03-19 18:21:00 -05:00
parent 9de4227ec1
commit 1802bcf796
4 changed files with 25 additions and 29 deletions
+11 -20
View File
@@ -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"
+8 -3
View File
@@ -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
+5 -3
View File
@@ -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}>
+1 -3
View File
@@ -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;
}