mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
explorer: implement file renaming
This commit is contained in:
@@ -60,6 +60,7 @@
|
||||
"spdx-satisfies": "^5.0.0",
|
||||
"typed-redux-saga": "^1.4.0",
|
||||
"typescript": "~4.6.2",
|
||||
"usehooks-ts": "^2.4.2",
|
||||
"web-vitals": "^2.1.4",
|
||||
"xterm": "^4.18.0",
|
||||
"xterm-addon-fit": "^0.5.0",
|
||||
|
||||
@@ -12,14 +12,22 @@ import {
|
||||
TreeNodeInfo,
|
||||
} from '@blueprintjs/core';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
import {
|
||||
fileStorageArchiveAllFiles,
|
||||
fileStorageExportFile,
|
||||
} from '../fileStorage/actions';
|
||||
import { useSelector } from '../reducers';
|
||||
import NewFileWizard from './NewFileWizard';
|
||||
import RenameFileDialog from './RenameFileDialog';
|
||||
import { explorerDeleteFile, explorerImportFiles } from './actions';
|
||||
import { ExplorerStringId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
@@ -68,8 +76,21 @@ const FileActionButtonGroup = forwardRef<
|
||||
>((props, ref) => {
|
||||
const dispatch = useDispatch();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({ setVisible }));
|
||||
// HACK: Hide buttons if file is removed from storage. Without this, if a
|
||||
// file is renamed to a new name then renamed again to the original name,
|
||||
// the buttons will be showing even though the list item is not hovered
|
||||
// because the list item was removed before the mouseleave event was
|
||||
// received.
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
useEffect(() => {
|
||||
if (!fileNames.includes(props.fileName)) {
|
||||
setVisible(false);
|
||||
}
|
||||
}, [fileNames, props.fileName, setVisible]);
|
||||
|
||||
useImperativeHandle(ref, () => ({ setVisible }), [setVisible]);
|
||||
|
||||
return (
|
||||
<ButtonGroup minimal={true} style={visible ? {} : { display: 'none' }}>
|
||||
@@ -77,7 +98,12 @@ const FileActionButtonGroup = forwardRef<
|
||||
icon="edit"
|
||||
toolTipId={ExplorerStringId.TreeItemRenameTooltip}
|
||||
toolTipReplacements={{ fileName: props.fileName }}
|
||||
onClick={() => alert('not implemented')}
|
||||
onClick={() => setIsRenameDialogOpen(true)}
|
||||
/>
|
||||
<RenameFileDialog
|
||||
oldName={props.fileName}
|
||||
isOpen={isRenameDialogOpen}
|
||||
onClose={() => setIsRenameDialogOpen(false)}
|
||||
/>
|
||||
<ActionButton
|
||||
// NB: the "import" icon has an arrow pointing down, which is
|
||||
@@ -140,10 +166,11 @@ const Header: React.VFC = () => {
|
||||
|
||||
const FileTree: React.VFC = () => {
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
const debouncedFileNames = useDebounce(fileNames);
|
||||
|
||||
const treeContents = useMemo(
|
||||
() =>
|
||||
[...fileNames].map<
|
||||
[...debouncedFileNames].map<
|
||||
TreeNodeInfo<{
|
||||
actionButtonGroupRef: React.RefObject<FileActionButtonGroupRef>;
|
||||
}>
|
||||
@@ -163,7 +190,7 @@ const FileTree: React.VFC = () => {
|
||||
nodeData: { actionButtonGroupRef },
|
||||
};
|
||||
}),
|
||||
[fileNames],
|
||||
[debouncedFileNames],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
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 { NewFileWizardStringId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
|
||||
type FileNameHelpTextProps = {
|
||||
/** The result of the file name validation. */
|
||||
validation: Exclude<FileNameValidationResult, FileNameValidationResult.Unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component that maps FileNameValidationResult to help message to display to user.
|
||||
*/
|
||||
const FileNameHelpText: React.VoidFunctionComponent<FileNameHelpTextProps> = (
|
||||
props,
|
||||
) => {
|
||||
const [i18n] = useI18n({ id: 'explorer', 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 FileNameFormGroupProps = {
|
||||
/** The file name in the input (without file extension). */
|
||||
readonly fileName: string;
|
||||
/** The file extension (including leading ".") */
|
||||
readonly fileExtension: string;
|
||||
/** 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component used to get a valid new file name.
|
||||
*/
|
||||
const FileNameFormGroup: React.VoidFunctionComponent<FileNameFormGroupProps> = (
|
||||
props,
|
||||
) => {
|
||||
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
|
||||
const [fileNameValidation, fileNameIntent] = useMemo(() => {
|
||||
const result = validateFileName(props.fileName, props.fileExtension, fileNames);
|
||||
|
||||
// can't call callback now because it would break react, so defer it
|
||||
setTimeout(() => props.onValidation(result), 0);
|
||||
|
||||
return [
|
||||
result,
|
||||
result === FileNameValidationResult.IsOk ? Intent.NONE : Intent.DANGER,
|
||||
];
|
||||
}, [props.fileName, props.fileExtension, fileNames]);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={i18n.translate(NewFileWizardStringId.FileNameLabel)}
|
||||
intent={fileNameIntent}
|
||||
subLabel={<FileNameHelpText validation={fileNameValidation} />}
|
||||
>
|
||||
<InputGroup
|
||||
aria-label="File name"
|
||||
value={props.fileName}
|
||||
inputRef={props.inputRef}
|
||||
intent={fileNameIntent}
|
||||
rightElement={<Tag>{props.fileExtension}</Tag>}
|
||||
onChange={(e) => props.onChange(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileNameFormGroup;
|
||||
@@ -6,10 +6,8 @@ import {
|
||||
Classes,
|
||||
Dialog,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Tag,
|
||||
} from '@blueprintjs/core';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useRef, useState } from 'react';
|
||||
@@ -17,9 +15,8 @@ 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';
|
||||
import en from './i18n.en.json';
|
||||
@@ -27,124 +24,42 @@ import en from './i18n.en.json';
|
||||
// This should be set to the most commonly used hub.
|
||||
const defaultHub = Hub.Technic;
|
||||
|
||||
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 = {
|
||||
/** Controls if the dialog is open. */
|
||||
readonly isOpen: boolean;
|
||||
/** Called when the dialog is closed. */
|
||||
readonly onClose: () => void;
|
||||
};
|
||||
|
||||
const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) => {
|
||||
const [i18n] = useI18n({ id: 'newFileWizard', translations: { en }, fallback: en });
|
||||
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
|
||||
const dispatch = useDispatch();
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [fileNameValidation, setFileNameValidation] = useState(
|
||||
FileNameValidationResult.IsEmpty,
|
||||
FileNameValidationResult.Unknown,
|
||||
);
|
||||
const [hubType, setHubType] = useState(defaultHub);
|
||||
|
||||
const fileNameInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const fileNameIntent =
|
||||
fileNameValidation === FileNameValidationResult.IsOk ? 'none' : 'danger';
|
||||
|
||||
const handleFileNameChanged = (fileName: string) => {
|
||||
setFileNameValidation(
|
||||
validateFileName(fileName, pythonFileExtension, fileNames),
|
||||
);
|
||||
setFileName(fileName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
icon="plus"
|
||||
title={i18n.translate(NewFileWizardStringId.Title)}
|
||||
isOpen={props.isOpen}
|
||||
onOpening={() => handleFileNameChanged('')}
|
||||
onOpening={() => setFileName('')}
|
||||
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>{pythonFileExtension}</Tag>}
|
||||
onChange={(e) => handleFileNameChanged(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FileNameFormGroup
|
||||
fileName={fileName}
|
||||
fileExtension={pythonFileExtension}
|
||||
inputRef={fileNameInputRef}
|
||||
onChange={(n) => setFileName(n)}
|
||||
onValidation={(r) => setFileNameValidation(r)}
|
||||
/>
|
||||
<FormGroup label={i18n.translate(NewFileWizardStringId.SmartHubLabel)}>
|
||||
<RadioGroup
|
||||
selectedValue={hubType}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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 { fileStorageRenameFile } from '../fileStorage/actions';
|
||||
import RenameFileDialog from './RenameFileDialog';
|
||||
|
||||
describe('rename button', () => {
|
||||
it('should close the dialog and dispatch an action when Rename is clicked', async () => {
|
||||
const onClose = jest.fn();
|
||||
const [dialog, dispatch] = testRender(
|
||||
<RenameFileDialog oldName="old.file" isOpen={true} onClose={onClose} />,
|
||||
);
|
||||
|
||||
const button = dialog.getByLabelText('Rename');
|
||||
|
||||
// have to type a new file name before Rename button is enabled
|
||||
const input = dialog.getByLabelText('File name');
|
||||
await waitFor(() => expect(input).toHaveFocus());
|
||||
userEvent.type(input, 'new');
|
||||
await waitFor(() => expect(button).not.toBeDisabled());
|
||||
|
||||
userEvent.click(button);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
fileStorageRenameFile('old.file', 'new.file'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be cancellable', async () => {
|
||||
const onClose = jest.fn();
|
||||
|
||||
const [dialog, dispatch] = testRender(
|
||||
<RenameFileDialog oldName="old.file" isOpen={true} onClose={onClose} />,
|
||||
);
|
||||
|
||||
const button = dialog.getByLabelText('Close');
|
||||
|
||||
await waitFor(() => expect(button).toBeVisible());
|
||||
|
||||
userEvent.click(button);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { Button, Classes, Dialog } from '@blueprintjs/core';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { fileStorageRenameFile } from '../fileStorage/actions';
|
||||
import { FileNameValidationResult } from '../pybricksMicropython/lib';
|
||||
import FileNameFormGroup from './FileNameFormGroup';
|
||||
import { RenameFileStringId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
|
||||
type RenameFileDialogProps = {
|
||||
/** The current file name (including file extension). */
|
||||
oldName: string;
|
||||
/** Controls the dialog open state. */
|
||||
isOpen: boolean;
|
||||
/** Called when the dialog is closed. */
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = (
|
||||
props,
|
||||
) => {
|
||||
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [baseName, extension] = props.oldName.split(/(\.\w+)$/);
|
||||
|
||||
const [newName, setNewName] = useState(baseName);
|
||||
const [result, setResult] = useState(FileNameValidationResult.Unknown);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={i18n.translate(RenameFileStringId.Title, {
|
||||
fileName: props.oldName,
|
||||
})}
|
||||
isOpen={props.isOpen}
|
||||
onOpening={() => setNewName(baseName)}
|
||||
onOpened={() => {
|
||||
inputRef.current?.select();
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
onClose={() => props.onClose()}
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<FileNameFormGroup
|
||||
fileName={newName}
|
||||
fileExtension={extension}
|
||||
inputRef={inputRef}
|
||||
onChange={(n) => setNewName(n)}
|
||||
onValidation={(r) => setResult(r)}
|
||||
/>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button
|
||||
aria-label="Rename"
|
||||
intent="primary"
|
||||
disabled={result !== FileNameValidationResult.IsOk}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
dispatch(
|
||||
fileStorageRenameFile(
|
||||
props.oldName,
|
||||
`${newName}${extension}`,
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n.translate(RenameFileStringId.ActionRename)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RenameFileDialog;
|
||||
@@ -31,5 +31,11 @@
|
||||
"action": {
|
||||
"create": "Create"
|
||||
}
|
||||
},
|
||||
"renameFile": {
|
||||
"title": "Rename '{fileName}'",
|
||||
"action": {
|
||||
"rename": "Rename"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { lookup } from '../../test';
|
||||
import { ExplorerStringId, NewFileWizardStringId } from './i18n';
|
||||
import { ExplorerStringId, NewFileWizardStringId, RenameFileStringId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
|
||||
describe('Ensure .json file has matches for ExplorerStringId', () => {
|
||||
@@ -16,3 +16,9 @@ describe('Ensure .json file has matches for NewFileWizardStringId', () => {
|
||||
expect(lookup(en, id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ensure .json file has matches for RenameFileStringId', () => {
|
||||
test.each(Object.values(RenameFileStringId))('%s', (id) => {
|
||||
expect(lookup(en, id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,3 +25,8 @@ export enum NewFileWizardStringId {
|
||||
SmartHubLabel = 'newFileWizard.smartHub.label',
|
||||
ActionCreate = 'newFileWizard.action.create',
|
||||
}
|
||||
|
||||
export enum RenameFileStringId {
|
||||
Title = 'renameFile.title',
|
||||
ActionRename = 'renameFile.action.rename',
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ 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. */
|
||||
@@ -40,7 +42,7 @@ export function validateFileName(
|
||||
fileName: string,
|
||||
extension: string,
|
||||
existingFiles: ReadonlyArray<string>,
|
||||
): FileNameValidationResult {
|
||||
): Exclude<FileNameValidationResult, FileNameValidationResult.Unknown> {
|
||||
if (existingFiles.includes(`${fileName}${extension}`)) {
|
||||
return FileNameValidationResult.AlreadyExists;
|
||||
}
|
||||
|
||||
@@ -12370,6 +12370,11 @@ use@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
|
||||
|
||||
usehooks-ts@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.4.2.tgz#a9a5df9d04dcce993d7e8ec088965ba44dfcd9cc"
|
||||
integrity sha512-YqD5HaloGpRSoNaXhAxR+CHTKda44mDtFqNJk5gRuy4qeHVxpp8ycN3LY0txYqNudCVuAk1f8m6M0ojQ5Aph8Q==
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
|
||||
Reference in New Issue
Block a user