Skip to content

Commit

Permalink
Ameerul / P2PS-1732 / Refactor FileUploaderComponent (#10650)
Browse files Browse the repository at this point in the history
* chore: migrate FileUploaderComponent to typescript

* fix: failing test cases

* chore: added suggestions

* chore: moved file-dropzone to p2p package

* chore: moved truncateFileName from shared to utils file and added test cases

* fix: regex potential DDos issue

* chore: added suggestions

* Update packages/p2p/src/components/file-dropzone/file-dropzone.tsx

Co-authored-by: Niloofar Sadeghi <93518187+niloofar-deriv@users.noreply.github.com>

* fix: import for types from file-dropzone

* fix: flickering Icon on tab switch

---------

Co-authored-by: Niloofar Sadeghi <93518187+niloofar-deriv@users.noreply.github.com>
  • Loading branch information
ameerul-deriv and niloofar-deriv committed Jan 4, 2024
1 parent 76d67b8 commit a8451d5
Show file tree
Hide file tree
Showing 21 changed files with 659 additions and 134 deletions.
1 change: 1 addition & 0 deletions packages/p2p/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"react": "^17.0.2",
"react-content-loader": "^6.2.0",
"react-dom": "^17.0.2",
"react-dropzone": "11.0.1",
"react-i18next": "^11.11.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@import '../file-dropzone.scss';

.fade-in-message {
@include file-dropzone-message;

&--enter-done {
opacity: 1;
transform: translate3d(0, 0, 0);
}

&--enter {
opacity: 0;
transform: translate3d(0, -16px, 0);
}

&--enter-active {
opacity: 1;
transform: translate3d(0, 0, 0);
}

&--exit {
opacity: 1;
transform: translate3d(0, 0, 0);
}

&--exit-active {
opacity: 0;
transform: translate3d(0, -16px, 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import { CSSTransition } from 'react-transition-group';
import { Text } from '@deriv/components';

type TFadeInMessage = {
is_visible: boolean;
color?: string;
key?: string;
timeout: number;
no_text?: boolean;
};

const FadeInMessage = ({
children,
color,
is_visible,
key,
no_text,
timeout,
}: React.PropsWithChildren<TFadeInMessage>) => (
<CSSTransition
appear
classNames={{
appear: 'fade-in-message--enter',
enter: 'fade-in-message--enter',
enterActive: 'fade-in-message--enter-active',
enterDone: 'fade-in-message--enter-done',
exit: 'fade-in-message--exit',
exitActive: 'fade-in-message--exit-active',
}}
in={is_visible}
key={key}
timeout={timeout}
unmountOnExit
>
{no_text ? (
<div className='fade-in-message'>{children}</div>
) : (
<Text align='center' className='fade-in-message' color={color || 'general'} size='xxs'>
{children}
</Text>
)}
</CSSTransition>
);

export default FadeInMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import FadeInMessage from './fade-in-message';
import './fade-in-message.scss';

export default FadeInMessage;
74 changes: 74 additions & 0 deletions packages/p2p/src/components/file-dropzone/file-dropzone.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
@mixin file-dropzone-message {
display: block;
max-width: 168px;
opacity: 1;
pointer-events: none;
position: absolute;
transform: translate3d(0, 0, 0);
transition: transform 0.25s ease, opacity 0.15s linear;

@include mobile {
max-width: 26rem;
}
}

.file-dropzone {
border-radius: $BORDER_RADIUS;
border: 1px dashed var(--border-normal);
color: var(--text-prominent);
cursor: pointer;
font-size: 1.25rem;
font-weight: bold;
height: 14rem;
padding: 2rem;
position: relative;
text-align: center;
width: 100%;

&__content {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
left: 0;
position: absolute;
top: 0;
width: 100%;
}

&__filename {
width: 100%;
}

&__message {
@include file-dropzone-message;
}

&--has-file {
border-style: solid;
border-color: var(--status-success);
background-color: var(--general-section-1);
}

&--has-error {
border-style: solid;
border-color: var(--status-danger);

.dc-file-dropzone__filename {
margin-top: -3em;
}
}

&--is-noclick {
cursor: auto;
}

&:hover,
&:focus {
outline: 0;
}

&:hover {
background-color: rgba(0, 0, 0, 0.025);
}
}
126 changes: 126 additions & 0 deletions packages/p2p/src/components/file-dropzone/file-dropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React from 'react';
import classNames from 'classnames';
import Dropzone, { DropzoneRef } from 'react-dropzone';
import { Text } from '@deriv/components';
import { TFileDropzone } from 'Types';
import { truncateFileName } from 'Utils/file-uploader';
import FadeInMessage from './fade-in-message';
import PreviewSingle from './preview-single';

const FileDropzone = ({ className, noClick = false, ...props }: TFileDropzone) => {
const {
accept,
error_message,
filename_limit,
hover_message,
max_size,
message,
multiple,
onDropAccepted,
onDropRejected,
validation_error_message,
value,
} = props;

const RenderErrorMessage = React.useCallback(
({ open }: DropzoneRef) => {
if (noClick && typeof message === 'function') return <>{message(open)}</>;

return <>{message}</>;
},
[message, noClick]
);

const RenderValidationErrorMessage = React.useCallback(
({ open }: DropzoneRef) => {
if (typeof validation_error_message === 'function') return <>{validation_error_message(open)}</>;

return <>{validation_error_message}</>;
},
[validation_error_message]
);

const dropzone_ref = React.useRef(null);

return (
<Dropzone
// accept prop is same as native HTML5 input accept - e.g - 'image/png'
accept={accept}
// set maximum size limit for file, in bytes (binary)
maxSize={max_size}
// allow multiple uploads
multiple={multiple || false}
// sends back accepted files array
onDropAccepted={onDropAccepted}
// sends back rejected files array
onDropRejected={onDropRejected}
noClick={noClick}
>
{({ getRootProps, getInputProps, isDragAccept, isDragActive, isDragReject, open }) => (
<div
{...getRootProps()}
className={classNames('file-dropzone', className, {
'file-dropzone--is-active': isDragActive,
'file-dropzone--has-file': isDragActive || value.length > 0,
'file-dropzone--has-error': (isDragReject || !!validation_error_message) && !isDragAccept,
'file-dropzone--is-noclick': noClick,
})}
ref={dropzone_ref}
>
<input {...getInputProps()} data-testid='dt_file_upload_input' />
<div className='file-dropzone__content'>
<FadeInMessage
// default message when not on hover or onDrag
is_visible={!isDragActive && !!message && value.length < 1 && !validation_error_message}
timeout={150}
no_text={noClick}
>
<RenderErrorMessage open={open} />
</FadeInMessage>
<FadeInMessage
// message shown on hover if files are accepted onDrag
is_visible={isDragActive && !isDragReject}
timeout={150}
>
{hover_message}
</FadeInMessage>
{/* Handle cases for displaying multiple files and single filenames */}
{multiple && value.length > 0 && !validation_error_message
? value.map((file, idx) => (
<Text
size='xxs'
weight='bold'
align='center'
key={file.name}
className='file-dropzone__filename'
>
{filename_limit ? truncateFileName(file, filename_limit) : file.name}
</Text>
))
: value[0] &&
!isDragActive &&
!validation_error_message && <PreviewSingle dropzone_ref={dropzone_ref} {...props} />}
<FadeInMessage
// message shown if there are errors with the dragged file
is_visible={isDragReject}
timeout={150}
color='loss-danger'
>
{error_message}
</FadeInMessage>
<FadeInMessage
// message shown on if there are validation errors with file uploaded
is_visible={!!validation_error_message && !isDragActive}
timeout={150}
color='loss-danger'
>
<RenderValidationErrorMessage open={open} />
</FadeInMessage>
</div>
</div>
)}
</Dropzone>
);
};

export default FileDropzone;
4 changes: 4 additions & 0 deletions packages/p2p/src/components/file-dropzone/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import FileDropzoneComponent from './file-dropzone';
import './file-dropzone.scss';

export default FileDropzoneComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import PreviewSingle from '../preview-single';

describe('<PreviewSingle />', () => {
const props = {
dropzone_ref: React.createRef(),
error_message: '',
hover_message: '',
onClickClose: jest.fn(),
value: [] as File[],
};

it('should render the Text component if preview_single is false', () => {
const file: File = new File(['hello'], 'hello.png', { type: 'image/png' });
props.value = [file];

render(<PreviewSingle {...props} />);

expect(screen.getByText('hello.png')).toBeInTheDocument();
});

it('should render the Image component if preview_single is true', () => {
const preview_single = <img data-testid='dt_image' src='hello.png' />;

render(<PreviewSingle {...props} preview_single={preview_single} />);

expect(screen.getByTestId('dt_image')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import PreviewSingle from './preview-single';
import './preview-single.scss';

export default PreviewSingle;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import '../file-dropzone.scss';

.preview-single {
&__filename {
width: 100%;
}

&__message {
@include file-dropzone-message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { RefObject } from 'react';
import { Text } from '@deriv/components';
import { TFileDropzone } from 'Types';
import { truncateFileName } from 'Utils/file-uploader';

type TPreviewSingle = {
dropzone_ref: RefObject<HTMLElement>;
} & TFileDropzone;

const PreviewSingle = (props: TPreviewSingle) => {
const { dropzone_ref, filename_limit, preview_single, value } = props;

if (preview_single) {
return <div className='preview-single__message'>{preview_single}</div>;
}

return (
<Text
align='center'
className='preview-single__filename'
size='xxs'
styles={{
maxWidth: `calc(${dropzone_ref.current?.offsetWidth || 365}px - 3.2rem)`,
}}
weight='bold'
>
{filename_limit ? truncateFileName(value[0], filename_limit) : value[0].name}
</Text>
);
};

export default PreviewSingle;
Loading

0 comments on commit a8451d5

Please sign in to comment.