Skip to content

Commit

Permalink
Merge pull request #3566 from marmelab/file-input-typescript
Browse files Browse the repository at this point in the history
[RFR] Migrate FileInput and related components to TypeScript
  • Loading branch information
fzaninotto authored Aug 21, 2019
2 parents 2489ac1 + c5fb5bf commit c3e806d
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 51 deletions.
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

0 comments on commit c3e806d

Please sign in to comment.