diff --git a/src/explorer/fileNameFormGroup/FileNameFormGroup.tsx b/src/explorer/fileNameFormGroup/FileNameFormGroup.tsx index ab0f4ce7..02bf4460 100644 --- a/src/explorer/fileNameFormGroup/FileNameFormGroup.tsx +++ b/src/explorer/fileNameFormGroup/FileNameFormGroup.tsx @@ -2,32 +2,108 @@ // Copyright (c) 2022 The Pybricks Authors import { Classes, FormGroup, InputGroup, Intent, Tag } from '@blueprintjs/core'; -import React from 'react'; +import type { AriaButtonProps } from '@react-types/button'; +import React, { useCallback, useRef } from 'react'; +import { useButton } from 'react-aria'; import { FileNameValidationResult } from '../../pybricksMicropython/lib'; import { I18nId, useI18n } from './i18n'; +/** + * Trims trailing and leading whitespace and replaces additional whitespace + * with underscores. + * @param value The input string. + * @returns The fixed up string. + */ +function replaceSpaces(value: string): string { + return value.trim().replaceAll(/\s+/g, '_'); +} + +/** + * Removes the file extension from a string. + * @param value The input string. + * @returns The fixed up string. + */ +function removeFileExtension(value: string): string { + return value.replace(/\.\w+$/, ''); +} + +/** + * Trims trailing and leading whitespace and replaces groups of invalid + * characters with underscores. + * @param value The input string. + * @returns The fixed up string. + */ +function replaceInvalidCharacters(value: string): string { + return value.trim().replaceAll(/[^A-Za-z0-9-_]+/g, '_'); +} + +type FixItButtonProps = Pick, 'onPress'>; + +const FixItButton: React.VoidFunctionComponent = (props) => { + const i18n = useI18n(); + const ref = useRef(null); + + const { buttonProps } = useButton( + { + ...props, + elementType: 'a', + }, + ref, + ); + + return {i18n.translate(I18nId.HelpTextFixIt)}; +}; + type FileNameHelpTextProps = { + /** The file name in the input (without file extension). */ + fileName: string; /** The result of the file name validation. */ validation: FileNameValidationResult; + /** Called when the "fix it" link is clicked. */ + onFix: (newName: string) => void; }; /** * Component that maps FileNameValidationResult to help message to display to user. */ const FileNameHelpText: React.VoidFunctionComponent = ({ + fileName, validation, + onFix, }) => { const i18n = useI18n(); + const handleHasSpaces = useCallback(() => { + onFix(replaceSpaces(fileName)); + }, [fileName, onFix]); + + const handleHasFileExtension = useCallback(() => { + onFix(removeFileExtension(fileName)); + }, [fileName, onFix]); + + const handleHasInvalidCharacters = useCallback(() => { + onFix(replaceInvalidCharacters(fileName)); + }, [fileName, onFix]); + switch (validation) { case FileNameValidationResult.IsOk: return <>{i18n.translate(I18nId.HelpTextIsOk)}; case FileNameValidationResult.IsEmpty: return <>{i18n.translate(I18nId.HelpTextIsEmpty)}; case FileNameValidationResult.HasSpaces: - return <>{i18n.translate(I18nId.HelpTextHasSpaces)}; + return ( + <> + {i18n.translate(I18nId.HelpTextHasSpaces)}{' '} + + + ); case FileNameValidationResult.HasFileExtension: - return <>{i18n.translate(I18nId.HelpTextHasFileExtension)}; + return ( + <> + {i18n.translate(I18nId.HelpTextHasFileExtension)}{' '} + + + ); case FileNameValidationResult.HasInvalidFirstCharacter: return ( <> @@ -45,7 +121,8 @@ const FileNameHelpText: React.VoidFunctionComponent = ({ numbers: 0…9, dash: -, underscore: _, - })} + })}{' '} + ); case FileNameValidationResult.AlreadyExists: @@ -87,7 +164,13 @@ const FileNameFormGroup: React.VoidFunctionComponent = ( } + subLabel={ + + } >