Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFR] Migrate FileInput and related components to TypeScript #3566

Merged
merged 3 commits into from
Aug 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import React, { Children, cloneElement, isValidElement } from 'react';
import React, {
FunctionComponent,
Children,
cloneElement,
isValidElement,
ReactElement,
} from 'react';
import PropTypes from 'prop-types';
import { shallowEqual } from 'recompose';
import { useDropzone } from 'react-dropzone';
import { useDropzone, DropzoneOptions } from 'react-dropzone';
import { makeStyles } from '@material-ui/core/styles';
import FormHelperText from '@material-ui/core/FormHelperText';
import classnames from 'classnames';
import { useInput, useTranslate } from 'ra-core';
import { useInput, useTranslate, InputProps } from 'ra-core';

import Labeled from './Labeled';
import FileInputPreview from './FileInputPreview';
Expand All @@ -25,20 +31,34 @@ const useStyles = makeStyles(theme => ({
root: { width: '100%' },
}));

const FileInput = ({
export interface FileInputProps {
accept?: string;
labelMultiple?: string;
labelSingle?: string;
maxSize?: number;
minSize?: number;
multiple?: boolean;
}

export interface FileInputOptions extends DropzoneOptions {
inputProps?: any;
}

const FileInput: FunctionComponent<
FileInputProps & InputProps<FileInputOptions>
> = ({
accept,
children,
classes: classesOverride,
className,
disableClick,
classes: classesOverride,
helperText,
label,
labelMultiple,
labelSingle,
labelMultiple = 'ra.input.file.upload_several',
labelSingle = 'ra.input.file.upload_single',
maxSize,
minSize,
multiple,
options = {},
multiple = false,
options: { inputProps: inputPropsOptions, ...options } = {},
placeholder,
resource,
source,
Expand All @@ -54,7 +74,9 @@ const FileInput = ({
return file;
}

const { source, title } = Children.only(children).props;
const { source, title } = (Children.only(children) as ReactElement<
any
>).props;

const preview = URL.createObjectURL(file);
const transformedFile = {
Expand All @@ -69,7 +91,7 @@ const FileInput = ({
return transformedFile;
};

const transformFiles = files => {
const transformFiles = (files: any[]) => {
if (!files) {
return multiple ? [] : null;
}
Expand Down Expand Up @@ -111,14 +133,14 @@ const FileInput = ({
const filteredFiles = files.filter(
stateFile => !shallowEqual(stateFile, file)
);
onChange(filteredFiles);
onChange(filteredFiles as any);
} else {
onChange(null);
}
};

const childrenElement = isValidElement(Children.only(children))
? Children.only(children)
? (Children.only(children) as ReactElement<any>)
: undefined;

const { getRootProps, getInputProps } = useDropzone({
Expand Down Expand Up @@ -148,9 +170,10 @@ const FileInput = ({
>
<input
id={id}
{...inputProps}
{...options.inputProps}
{...getInputProps()}
{...getInputProps({
...inputProps,
...inputPropsOptions,
})}
/>
{placeholder ? (
placeholder
Expand Down Expand Up @@ -196,7 +219,6 @@ FileInput.propTypes = {
children: PropTypes.element,
classes: PropTypes.object,
className: PropTypes.string,
disableClick: PropTypes.bool,
id: PropTypes.string,
isRequired: PropTypes.bool,
label: PropTypes.string,
Expand All @@ -211,11 +233,4 @@ FileInput.propTypes = {
placeholder: PropTypes.node,
};

FileInput.defaultProps = {
labelMultiple: 'ra.input.file.upload_several',
labelSingle: 'ra.input.file.upload_single',
multiple: false,
onUpload: () => {},
};

export default FileInput;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,21 @@ import { render, cleanup, fireEvent } from '@testing-library/react';
import FileInputPreview from './FileInputPreview';

describe('<FileInputPreview />', () => {
afterEach(cleanup);
beforeAll(() => {
// @ts-ignore
global.URL.revokeObjectURL = jest.fn();
});

afterAll(() => {
// @ts-ignore
delete global.URL.revokeObjectURL;
});

afterEach(() => {
// @ts-ignore
global.URL.revokeObjectURL.mockClear();
cleanup();
});

const file = {
preview: 'previewUrl',
Expand All @@ -13,7 +27,6 @@ describe('<FileInputPreview />', () => {
const defaultProps = {
file,
onRemove: jest.fn(),
revokeObjectURL: jest.fn(),
};

it('should call `onRemove` prop when clicking on remove button', () => {
Expand Down Expand Up @@ -41,36 +54,28 @@ describe('<FileInputPreview />', () => {
});

it('should clean up generated URLs for preview', () => {
const revokeObjectURL = jest.fn();

const { unmount } = render(
<FileInputPreview
{...defaultProps}
revokeObjectURL={revokeObjectURL}
>
<FileInputPreview {...defaultProps}>
<div id="child">Child</div>
</FileInputPreview>
);

unmount();
expect(revokeObjectURL).toHaveBeenCalledWith('previewUrl');
// @ts-ignore
expect(global.URL.revokeObjectURL).toHaveBeenCalledWith('previewUrl');
});

it('should not try to clean up preview urls if not passed a File object with a preview', () => {
const file = {};
const revokeObjectURL = jest.fn();

const { unmount } = render(
<FileInputPreview
{...defaultProps}
file={file}
revokeObjectURL={revokeObjectURL}
>
<FileInputPreview {...defaultProps} file={file}>
<div id="child">Child</div>
</FileInputPreview>
);

unmount();
expect(revokeObjectURL).not.toHaveBeenCalled();
// @ts-ignore
expect(global.URL.revokeObjectURL).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, ReactNode, FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core';
import RemoveCircle from '@material-ui/icons/RemoveCircle';
Expand All @@ -12,11 +12,17 @@ const useStyles = makeStyles(theme => ({
},
}));

const FileInputPreview = ({
interface Props {
children: ReactNode;
className?: string;
onRemove: () => void;
file: any;
}

const FileInputPreview: FunctionComponent<Props> = ({
children,
className,
onRemove,
revokeObjectURL,
file,
...rest
}) => {
Expand All @@ -28,12 +34,10 @@ const FileInputPreview = ({
const preview = file.rawFile ? file.rawFile.preview : file.preview;

if (preview) {
revokeObjectURL
? revokeObjectURL(preview)
: window.URL.revokeObjectURL(preview);
window.URL.revokeObjectURL(preview);
}
};
}, [file, revokeObjectURL]);
}, [file]);

return (
<div className={className} {...rest}>
Expand All @@ -55,7 +59,6 @@ FileInputPreview.propTypes = {
className: PropTypes.string,
file: PropTypes.object,
onRemove: PropTypes.func.isRequired,
revokeObjectURL: PropTypes.func,
};

FileInputPreview.defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';

import FileInput from './FileInput';
import FileInput, { FileInputProps, FileInputOptions } from './FileInput';
import { InputProps } from 'ra-core';

const useStyles = makeStyles(theme => ({
root: { width: '100%' },
Expand Down Expand Up @@ -32,7 +33,7 @@ const useStyles = makeStyles(theme => ({
},
}));

const ImageInput = props => {
const ImageInput = (props: FileInputProps & InputProps<FileInputOptions>) => {
const classes = useStyles(props);

return (
Expand Down