Skip to content
This repository was archived by the owner on Feb 23, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 5 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
8 changes: 4 additions & 4 deletions src/Containers/DropArea/DropArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface IDropAreaProps extends DropzoneType {
dropzoneProps?: DropzoneOptions;
}

export const DropArea: React.FC<IDropAreaProps> = ({
export const DropArea = React.forwardRef<HTMLDivElement,IDropAreaProps>(({
message = 'Drag & Drop your files here',
buttonText = 'Browse Files',
onClickHandler = () => null,
Expand All @@ -66,7 +66,7 @@ export const DropArea: React.FC<IDropAreaProps> = ({
width,
dropzoneProps = {},
...props
}): React.ReactElement => {
},dropAreaRef): React.ReactElement => {
const { getInputProps, getRootProps, isDragActive, open } = useDropzone({
onDragEnter,
onDragLeave,
Expand Down Expand Up @@ -95,7 +95,7 @@ export const DropArea: React.FC<IDropAreaProps> = ({
return (
<Dropzone multiple {...props}>
{() => (
<RootDiv {...getRootProps({})} isDisabled={isDisabled}>
<RootDiv {...getRootProps({})} isDisabled={isDisabled} ref={dropAreaRef}>
<DropAreaBox
isDragEnter={isDragActive}
width={width}
Expand All @@ -118,7 +118,7 @@ export const DropArea: React.FC<IDropAreaProps> = ({
)}
</Dropzone>
);
};
});

const RootDiv = styled.div<{ isDisabled: boolean }>`
width: fit-content;
Expand Down
24 changes: 24 additions & 0 deletions src/Containers/FileUploadV2/FileUploadV2.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { Meta, Story } from '@storybook/react';
import { FileUploadV2, IFileUploadV2Props } from './FileUploadV2';

export default {
title: 'Components/FileUploadV2',
component: FileUploadV2,
args: {
onFile:(base64StringFile:string)=>{console.log(base64StringFile)},
dropAreaProps:{
width:400
}
},
} as Meta;

export const Basic: Story<IFileUploadV2Props> = (args) => (
<FileUploadV2 {...args} />
);

export const TestIsFailure = Basic.bind({});
TestIsFailure.args = {
...Basic.args,
isTestIsFailure:true
};
256 changes: 256 additions & 0 deletions src/Containers/FileUploadV2/FileUploadV2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import React, { useState, useCallback, useEffect,useRef } from 'react';
import styled from 'styled-components';
// @ts-ignore
import worker from 'workerize-loader!./worker'; // eslint-disable-line
import { useMounted } from '@Utils/Hooks';
import { MainInterface, Main } from '@Utils/BaseStyles';
import {Button} from '@Inputs/Button/Button';
import { PanelCard,OperationState } from '../PanelCard/PanelCard';
import {DropArea,IDropAreaProps} from '../DropArea/DropArea';

// TODO: Add animations if possible (height transitions of the container component (expansion-contraction))
// and fade-in, fade-out effect of the informative panels.

const MESSAGE_DURATION = 1500;
const NO_BASE64STRINGFILE = 'NO_BASE64STRINGFILE';

interface IPanel {
/** whether it's loading file, is completed, is failure */
operationState: OperationState;
/** name of file associated with the informative panel */
name: string;
/** worker; will do the job of reading the file */
worker: Worker | null;
/** the file associated with the informative panel */
file: File | null;
}

interface IInformativePanels {
/** array of panels */
panels: IPanel[];
/** names of files already uploaded, or failed, or cancelled */
makeItDisappear: string[];
/** names of files for which we want to start workers */
startWorkers: string[];
}

export interface IFileUploadV2Props
extends MainInterface,
React.HTMLAttributes<HTMLDivElement> {
/** if true, failure message will appear even after success operation; its purpose is to test the appearance of the failure message during development */
isTestIsFailure?: boolean;
/**
* function to process the file read and transformed to a base64 string; default: does nothing
* @param {string} base64String the file read and transformed to a base64 string
*/
onFile?: (base64String: string) => void;
/** time in ms of the presence of the bottom panel informing the result of the operation (sucess or failure); default value: 1500 */
messageDuration?: number;
dropAreaProps?:IDropAreaProps;
}
/**
* multiple file upload, in parallel, version 2
*/
export const FileUploadV2: React.FC<IFileUploadV2Props> = ({
isTestIsFailure = false,
onFile = (base64String: string) => null,
messageDuration = MESSAGE_DURATION,
dropAreaProps={},
...props
}): React.ReactElement => {
const isMounted = useMounted();
const [informativePanels, setInformativePanels] =
useState<IInformativePanels>({
panels: [],
makeItDisappear: [],
startWorkers: [],
});
const [dropAreaWidth, setDropAreaWidth] = useState<number|undefined>();
const dropAreaRef=useRef<HTMLDivElement>(null);

/**
* set end state
*/
const prepareForEndInformativePanel = useCallback(
(operationState: OperationState, informativePanel: IPanel): void => {
setInformativePanels((prev) => ({
...prev,
panels: prev.panels.map((panel) => {
if (panel.name === informativePanel.name)
return {
...panel,
operationState,
};
return panel;
}),
makeItDisappear: [
...prev.makeItDisappear,
informativePanel.name,
],
}));
},
[],
);

/**
* terminate worker and set state of informative panel to success or failure and
* send order to remove informative panel in the future. also do whatever user
* wants to do with the file read in case of success
*/
const onWorkerMessage = useCallback(
(e: any) => {
const { base64StringFile, name } = e.data;
if (base64StringFile === undefined) {
return;
}
const informativePanel = informativePanels.panels.find(
(panel) => panel.name === name,
);
if (informativePanel) {
if (
base64StringFile === NO_BASE64STRINGFILE ||
isTestIsFailure
) {
prepareForEndInformativePanel(
OperationState.isFailure,
informativePanel,
);
} else {
onFile(base64StringFile);
prepareForEndInformativePanel(
OperationState.isSuccess,
informativePanel,
);
}
}
},
[
informativePanels.panels,
isTestIsFailure,
onFile,
prepareForEndInformativePanel,
],
);

/**
* load array of informative panels and send order to start workers
*/
const onDrop = useCallback((acceptedFiles: File[]) => {
const newInformativePanels = acceptedFiles.map((file) => {
const workerInstance = worker();
return {
operationState: OperationState.isLoading,
name: file.name,
worker: workerInstance,
file,
};
});
const fileNames = acceptedFiles.map((file) => file.name);
setInformativePanels((prev) => ({
...prev,
panels: [...prev.panels, ...newInformativePanels],
startWorkers: [...fileNames],
}));
}, []);

// start workers after files have been droped and array of informative panels
// are loaded
useEffect(() => {
if (informativePanels.startWorkers.length) {
informativePanels.startWorkers.forEach((name) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate code use destructing

const informativePanel = informativePanels.panels.find(
(panel) => panel.name === name,
);
if (informativePanel && informativePanel.worker) {
informativePanel.worker.onmessage = onWorkerMessage;
informativePanel.worker?.postMessage({
file: informativePanel.file,
});
}
});
setInformativePanels((prev) => ({
...prev,
startWorkers: [],
}));
}
}, [informativePanels.startWorkers.length]);

// make disappear informative panels in the future
useEffect(() => {
if (informativePanels.makeItDisappear.length) {
informativePanels.makeItDisappear.forEach((name) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean up functions

setTimeout(() => {
if (isMounted.current) {
setInformativePanels((prev) => ({
...prev,
panels: prev.panels.filter((panel) => {
if (panel.name === name) {
panel.worker?.terminate();
return false;
}
return true;
}),
makeItDisappear: prev.makeItDisappear.filter(
(name_) => name_ !== name,
),
}));
}
}, messageDuration);
});
}
}, [informativePanels.makeItDisappear.length]);

const onCancelUploading = (name: string) => () => {
setInformativePanels((prev) => ({
...prev,
panels: prev.panels.filter((panel) => {
if (panel.name === name) {
panel.worker?.terminate();
return false;
}
return true;
}),
}));
};

// terminate workers on clean up function
useEffect(
() => () => {
if (!isMounted.current) {
informativePanels.panels.forEach((panel) =>
panel.worker?.terminate(),
);
}
},
[informativePanels.panels],
);

useEffect(()=>{
if(dropAreaRef.current){
setDropAreaWidth(dropAreaRef.current.getBoundingClientRect().width);
}
},[])

return (
<FileUploadV2Container {...props}>
<DropArea onDropHandler={onDrop} {...dropAreaProps} ref={dropAreaRef} />
{informativePanels.panels.map((panel) => (
<PanelCard
key={panel.name}
cancelButtonOnLoading={<Button onClick={onCancelUploading(panel.name)}>Cancel</Button>}
name={panel.name}
operationState={panel.operationState}
margin="10px 0"
style={{width:dropAreaWidth,boxSizing:'border-box'}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styled comp

/>
))}
</FileUploadV2Container>
);
};

const FileUploadV2Container = styled.div<MainInterface>`
background-color: ${({theme})=>theme.colors.background};
border-radius: ${({theme})=>theme.dimensions.radius};
width: fit-content;
${({theme,...props }): string => Main({ padding:theme.dimensions.padding.container,...props })}
`;
24 changes: 24 additions & 0 deletions src/Containers/FileUploadV2/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
onmessage = (e) => {
const { file } = e.data;
const reader = new FileReader();
reader.onload = () => {
let base64StringFile = 'NO_BASE64STRINGFILE';
if (reader.result) {
if (typeof reader.result === 'string') {
base64StringFile = btoa(reader.result);
} else {
const bytes = Array.from(new Uint8Array(reader.result));
base64StringFile = btoa(
bytes.map((item) => String.fromCharCode(item)).join(''),
);
}
}
postMessage({ base64StringFile,name:file.name });
};
try{
reader.readAsArrayBuffer(file);
}catch(error){
console.log(error);
postMessage({});
}
};
1 change: 1 addition & 0 deletions src/Containers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ export * from './StoreSelector/StoreSelector';
export * from './ScreenFlashEffect/ScreenFlashEffect';
export * from './PanelCard/PanelCard';
export * from './DropArea/DropArea';
export * from './FileUploadV2/FileUploadV2';