Skip to content

Commit

Permalink
feat(form): add isValidFileName option to useFileUpload
Browse files Browse the repository at this point in the history
This allows files without extensions like LICENSE to be uploaded
with other text-like files
  • Loading branch information
mlaursen committed Sep 10, 2021
1 parent 9238140 commit dbd0375
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 3 deletions.
62 changes: 62 additions & 0 deletions packages/form/src/file-input/__tests__/useFileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
FileValidationError,
isFileSizeError,
isTooManyFilesError,
IsValidFileName,
isValidFileName,
TooManyFilesError,
} from "../utils";

Expand Down Expand Up @@ -633,9 +635,69 @@ describe("useFileUpload", () => {
totalBytes: 0,
totalFiles: 0,
totalFileSize: MAX_UPLOAD_SIZE,
isValidFileName,
});
});

it("should allow for a custom isValidFileName so that files without extensions can be uploaded", () => {
const allowExtensionsAndLicense: IsValidFileName = (
file,
extensionRegExp,
extensions
) =>
isValidFileName(file, extensionRegExp, extensions) ||
/^LICENSE$/i.test(file.name);

const customIsValidFileName = jest.fn(allowExtensionsAndLicense);
const extensions = ["md", "txt"];
const extensionRegExp = new RegExp("\\.(md|txt)$", "i");

const { getByLabelText } = renderComplex({
extensions,
isValidFileName: customIsValidFileName,
});

const input = getByLabelText(/Upload/) as HTMLInputElement;
expect(customIsValidFileName).not.toBeCalled();

const md = createFile("file2.md", 1024);
userEvent.upload(input, md);
expect(customIsValidFileName).toBeCalledWith(
md,
extensionRegExp,
extensions
);
expect(getErrorDialog).toThrow();

const txt = createFile("file2.txt", 1024);
userEvent.upload(input, txt);
expect(customIsValidFileName).toBeCalledWith(
txt,
extensionRegExp,
extensions
);
expect(getErrorDialog).toThrow();

const license = createFile("LICENSE", 1024);
userEvent.upload(input, license);
expect(customIsValidFileName).toBeCalledWith(
license,
extensionRegExp,
extensions
);
expect(getErrorDialog).toThrow();

const png = createFile("file1.png", 1024);
userEvent.upload(input, png);
expect(customIsValidFileName).toBeCalledWith(
png,
extensionRegExp,
extensions
);
expect(getErrorDialog).not.toThrow();
fireEvent.click(getByRoleGlobal(document.body, "button", { name: "Okay" }));
});

it("should throw a TooManyFilesError if too many files are uploaded", () => {
const { getByLabelText, getByText } = renderComplex({ maxFiles: 1 });
const input = getByLabelText(/Upload/) as HTMLInputElement;
Expand Down
3 changes: 3 additions & 0 deletions packages/form/src/file-input/__tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isFileAccessError,
isFileExtensionError,
isGenericFileError,
isValidFileName,
validateFiles,
} from "../utils";

Expand Down Expand Up @@ -145,6 +146,7 @@ describe("validateFiles", () => {
totalBytes: 0,
totalFiles: 0,
totalFileSize: 1024,
isValidFileName,
});

const result2 = validateFiles([file1, file3], {
Expand All @@ -155,6 +157,7 @@ describe("validateFiles", () => {
totalBytes: 0,
totalFiles: 0,
totalFileSize: 2000,
isValidFileName,
});

expect(result1).toEqual({
Expand Down
4 changes: 4 additions & 0 deletions packages/form/src/file-input/useFileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
FilesValidator,
GetFileParser,
ProcessingFileUploadStats,
isValidFileName as defaultIsValidFileName,
validateFiles as defaultValidateFiles,
FileValidationOptions,
} from "./utils";
Expand Down Expand Up @@ -220,6 +221,7 @@ export function useFileUpload<E extends HTMLElement, CustomError = never>({
onChange: propOnChange,
validateFiles = defaultValidateFiles,
getFileParser = defaultGetFileParser,
isValidFileName = defaultIsValidFileName,
}: FileUploadOptions<E, CustomError> = {}): Readonly<
FileUploadHookReturnValue<E, CustomError>
> {
Expand Down Expand Up @@ -364,6 +366,7 @@ export function useFileUpload<E extends HTMLElement, CustomError = never>({
totalBytes,
totalFiles,
totalFileSize,
isValidFileName,
});

dispatch({ type: "queue", errors, files: pending });
Expand All @@ -377,6 +380,7 @@ export function useFileUpload<E extends HTMLElement, CustomError = never>({
totalBytes,
totalFiles,
totalFileSize,
isValidFileName,
]
);
const onDrop = useCallback(
Expand Down
37 changes: 34 additions & 3 deletions packages/form/src/file-input/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,33 @@ export function isFileExtensionError<CustomError>(
return "name" in error && error.name === "FileExtensionError";
}

/**
* This function is used to determine if a file should be added to the
* {@link FileExtensionError}. The default implementation should work for most
* use cases except when files that do not have extensions can be uploaded. i.e.
* LICENSE files.
*
* @param file - The file being checked
* @param extensionRegExp - A regex that will only be defined if the
* `extensions` list had at least one value.
* @param extensions - The list of extensions allowed
* @returns true if the file has a valid name.
* @remarks \@since 3.1.0
*/
export type IsValidFileName = (
file: File,
extensionRegExp: RegExp | undefined,
extendsions: readonly string[]
) => boolean;

/**
*
* @defaultValue `matcher?.test(file.name) ?? true`
* @remarks \@since 3.1.0
*/
export const isValidFileName: IsValidFileName = (file, matcher) =>
matcher?.test(file.name) ?? true;

/** @remarks \@since 2.9.0 */
export interface FileValidationOptions {
/**
Expand Down Expand Up @@ -293,6 +320,9 @@ export interface FileValidationOptions {
*/
extensions?: readonly string[];

/** {@inheritDoc IsValidFileName} */
isValidFileName?: IsValidFileName;

/**
* An optional total file size to enforce when the {@link maxFiles} option is
* not set to `1`.
Expand Down Expand Up @@ -399,12 +429,13 @@ export function validateFiles<CustomError>(
totalBytes,
totalFiles,
totalFileSize,
isValidFileName,
}: FilesValidationOptions
): ValidatedFilesResult<CustomError> {
const errors: FileValidationError<CustomError>[] = [];
const pending: File[] = [];
const extraFiles: File[] = [];
const nameRegExp =
const extensionRegExp =
extensions.length > 0
? new RegExp(`\\.(${extensions.join("|")})$`, "i")
: undefined;
Expand All @@ -423,8 +454,8 @@ export function validateFiles<CustomError>(
}

let valid = true;
const { name, size } = file;
if (nameRegExp && !nameRegExp.test(name)) {
const { size } = file;
if (!isValidFileName(file, extensionRegExp, extensions)) {
valid = false;
extensionErrors.push(file);
}
Expand Down

0 comments on commit dbd0375

Please sign in to comment.