From bbcabb9409511adce5cadcfc8802862982907fad Mon Sep 17 00:00:00 2001 From: roggc Date: Sat, 22 Jan 2022 00:45:27 +0100 Subject: [PATCH 1/6] create files --- src/Containers/FileUploadV2/FileUploadV2.stories.tsx | 0 src/Containers/FileUploadV2/FileUploadV2.tsx | 0 src/Containers/FileUploadV2/worker.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/Containers/FileUploadV2/FileUploadV2.stories.tsx create mode 100644 src/Containers/FileUploadV2/FileUploadV2.tsx create mode 100644 src/Containers/FileUploadV2/worker.ts diff --git a/src/Containers/FileUploadV2/FileUploadV2.stories.tsx b/src/Containers/FileUploadV2/FileUploadV2.stories.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/Containers/FileUploadV2/FileUploadV2.tsx b/src/Containers/FileUploadV2/FileUploadV2.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/Containers/FileUploadV2/worker.ts b/src/Containers/FileUploadV2/worker.ts new file mode 100644 index 000000000..e69de29bb From 6963b0c871773073941cdfa6d3aaddaa3c89b849 Mon Sep 17 00:00:00 2001 From: roggc Date: Sat, 22 Jan 2022 00:46:47 +0100 Subject: [PATCH 2/6] fill fileuploadv2.tsx --- src/Containers/FileUploadV2/FileUploadV2.tsx | 323 +++++++++++++++++++ 1 file changed, 323 insertions(+) diff --git a/src/Containers/FileUploadV2/FileUploadV2.tsx b/src/Containers/FileUploadV2/FileUploadV2.tsx index e69de29bb..080de3b94 100644 --- a/src/Containers/FileUploadV2/FileUploadV2.tsx +++ b/src/Containers/FileUploadV2/FileUploadV2.tsx @@ -0,0 +1,323 @@ +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 Dropzone, { useDropzone } from 'react-dropzone'; +import { MainInterface, Main } from '@Utils/BaseStyles'; + +// 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'; + +export enum OperationState { + isSuccess, + isFailure, + isLoading, + isUnknown, +} + +/** + * minimum interface for panels used in fileupload component + */ +export interface IPanelProps extends MainInterface,React.HTMLAttributes { + /** function executed for cancelling file upload */ + onCancelUploading?: () => void; + /** state of the operation: isSuccess,isLoading,isFailure */ + operationState?: OperationState; + messageIsSuccess?: string; + messageIsFailure?: string; + messageIsLoading?: string; + /** name of file */ + name?: string; +} + +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 IDropAreaProps + extends MainInterface, + React.HTMLAttributes { + isDragEnter?: boolean; + message?: string; +} + +export interface IFileUploadV2Props + extends MainInterface, + React.HTMLAttributes { + /** whether or not the file upload component is disabled */ + isDisabled?: boolean; + /** component to render the drop area */ + DropArea: React.FC; + dropAreaProps?: IDropAreaProps; + /** component to render the informative panel */ + Panel: React.FC; + /** 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 + */ + processFile?: (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; +} +/** + * multiple file upload, in parallel, version 2 + */ +export const FileUploadV2: React.FC = ({ + isDisabled = false, + DropArea, + dropAreaProps = { padding: '10px' }, + Panel, + isTestIsFailure = false, + processFile = (base64String: string) => null, + messageDuration = MESSAGE_DURATION, + padding = '10px', + ...props +}): React.ReactElement => { + const isMounted = useMounted(); + const [informativePanels, setInformativePanels] = + useState({ + panels: [], + makeItDisappear: [], + startWorkers: [], + }); + const [isDragEnter, setIsDragEnter] = useState(false); + const dropAreaRef = useRef(null); + const [width, setWidth] = useState(0); + + /** + * 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 { + processFile(base64StringFile); + prepareForEndInformativePanel( + OperationState.isSuccess, + informativePanel, + ); + } + } + }, + [ + informativePanels.panels, + isTestIsFailure, + processFile, + prepareForEndInformativePanel, + ], + ); + + const onDragEnter = () => { + setIsDragEnter(true); + }; + + const onDragLeave = () => { + setIsDragEnter(false); + }; + + /** + * 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], + })); + setIsDragEnter(false); + }, []); + + // start workers after files have been droped and array of informative panels + // are loaded + useEffect(() => { + if (informativePanels.startWorkers.length) { + informativePanels.startWorkers.forEach((name) => { + 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) => { + 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 { getRootProps, getInputProps } = useDropzone({ + onDrop, + onDragEnter, + onDragLeave, + disabled: isDisabled, + }); + + const onCancelUploading = (name: string) => () => { + setInformativePanels((prev) => ({ + ...prev, + panels: prev.panels.filter((panel) => { + if (panel.name === name) { + panel.worker?.terminate(); + return false; + } + return true; + }), + })); + }; + + // after first render, we compute width of the drop area component + useEffect(() => { + if (dropAreaRef.current) { + setWidth(dropAreaRef.current.getBoundingClientRect().width); + } + }, []); + + // terminate workers on clean up function + useEffect( + () => () => { + if (!isMounted.current) { + informativePanels.panels.forEach((panel) => + panel.worker?.terminate(), + ); + } + }, + [informativePanels.panels], + ); + + return ( + + + {() => ( +
+ + +
+ )} +
+ {informativePanels.panels.map((panel) => ( + + ))} +
+ ); +}; + +const FileUploadV2Container = styled.div` + background-color: white; + border-radius: 10px; + width: fit-content; + ${({ ...props }): string => ` + ${Main({ ...props })} + `} +`; From f5e3ca7a55f9e7897b30b52aec1e38889427fe9a Mon Sep 17 00:00:00 2001 From: roggc Date: Sat, 22 Jan 2022 00:47:41 +0100 Subject: [PATCH 3/6] fill fileuploadv2.stories.tsx --- .../FileUploadV2/FileUploadV2.stories.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Containers/FileUploadV2/FileUploadV2.stories.tsx b/src/Containers/FileUploadV2/FileUploadV2.stories.tsx index e69de29bb..cc1324f32 100644 --- a/src/Containers/FileUploadV2/FileUploadV2.stories.tsx +++ b/src/Containers/FileUploadV2/FileUploadV2.stories.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Meta, Story } from '@storybook/react'; +import { FileUploadV2, IFileUploadV2Props } from './FileUploadV2'; +import {PanelCard} from '../Panel/Panel'; +import {DropArea} from '../DropArea/DropArea'; + +export default { + title: 'Components/FileUploadV2', + component: FileUploadV2, + args: { + DropArea, + Panel:PanelCard, + processFile:(base64StringFile:string)=>{console.log(base64StringFile)} + }, +} as Meta; + +export const Basic: Story = (args) => ( + +); + +export const TestIsFailure = Basic.bind({}); +TestIsFailure.args = { + ...Basic.args, + isTestIsFailure:true +}; From df6ea739de7ae214bfd8f6a0facea9488a6bddbc Mon Sep 17 00:00:00 2001 From: roggc Date: Sat, 22 Jan 2022 00:48:21 +0100 Subject: [PATCH 4/6] fill worker.ts --- src/Containers/FileUploadV2/worker.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Containers/FileUploadV2/worker.ts b/src/Containers/FileUploadV2/worker.ts index e69de29bb..17a094d92 100644 --- a/src/Containers/FileUploadV2/worker.ts +++ b/src/Containers/FileUploadV2/worker.ts @@ -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({}); + } +}; \ No newline at end of file From 60e20ce629bbd0a6f8294bda9892bf42959db246 Mon Sep 17 00:00:00 2001 From: roggc Date: Sat, 22 Jan 2022 02:41:13 +0100 Subject: [PATCH 5/6] create fileuploadv2 component --- src/Containers/DropArea/DropArea.tsx | 8 +- .../FileUploadV2/FileUploadV2.stories.tsx | 9 +- src/Containers/FileUploadV2/FileUploadV2.tsx | 121 ++++-------------- src/Containers/index.ts | 1 + 4 files changed, 36 insertions(+), 103 deletions(-) diff --git a/src/Containers/DropArea/DropArea.tsx b/src/Containers/DropArea/DropArea.tsx index 4a300cb35..78c22dcb0 100644 --- a/src/Containers/DropArea/DropArea.tsx +++ b/src/Containers/DropArea/DropArea.tsx @@ -54,7 +54,7 @@ export interface IDropAreaProps extends DropzoneType { dropzoneProps?: DropzoneOptions; } -export const DropArea: React.FC = ({ +export const DropArea = React.forwardRef(({ message = 'Drag & Drop your files here', buttonText = 'Browse Files', onClickHandler = () => null, @@ -66,7 +66,7 @@ export const DropArea: React.FC = ({ width, dropzoneProps = {}, ...props -}): React.ReactElement => { +},dropAreaRef): React.ReactElement => { const { getInputProps, getRootProps, isDragActive, open } = useDropzone({ onDragEnter, onDragLeave, @@ -95,7 +95,7 @@ export const DropArea: React.FC = ({ return ( {() => ( - + = ({ )} ); -}; +}); const RootDiv = styled.div<{ isDisabled: boolean }>` width: fit-content; diff --git a/src/Containers/FileUploadV2/FileUploadV2.stories.tsx b/src/Containers/FileUploadV2/FileUploadV2.stories.tsx index cc1324f32..9e801aa7a 100644 --- a/src/Containers/FileUploadV2/FileUploadV2.stories.tsx +++ b/src/Containers/FileUploadV2/FileUploadV2.stories.tsx @@ -1,16 +1,15 @@ import React from 'react'; import { Meta, Story } from '@storybook/react'; import { FileUploadV2, IFileUploadV2Props } from './FileUploadV2'; -import {PanelCard} from '../Panel/Panel'; -import {DropArea} from '../DropArea/DropArea'; export default { title: 'Components/FileUploadV2', component: FileUploadV2, args: { - DropArea, - Panel:PanelCard, - processFile:(base64StringFile:string)=>{console.log(base64StringFile)} + onFile:(base64StringFile:string)=>{console.log(base64StringFile)}, + dropAreaProps:{ + width:400 + } }, } as Meta; diff --git a/src/Containers/FileUploadV2/FileUploadV2.tsx b/src/Containers/FileUploadV2/FileUploadV2.tsx index 080de3b94..74bf1fe5b 100644 --- a/src/Containers/FileUploadV2/FileUploadV2.tsx +++ b/src/Containers/FileUploadV2/FileUploadV2.tsx @@ -1,10 +1,12 @@ -import React, { useState, useCallback, useEffect, useRef } from 'react'; +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 Dropzone, { useDropzone } from 'react-dropzone'; 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. @@ -12,28 +14,6 @@ import { MainInterface, Main } from '@Utils/BaseStyles'; const MESSAGE_DURATION = 1500; const NO_BASE64STRINGFILE = 'NO_BASE64STRINGFILE'; -export enum OperationState { - isSuccess, - isFailure, - isLoading, - isUnknown, -} - -/** - * minimum interface for panels used in fileupload component - */ -export interface IPanelProps extends MainInterface,React.HTMLAttributes { - /** function executed for cancelling file upload */ - onCancelUploading?: () => void; - /** state of the operation: isSuccess,isLoading,isFailure */ - operationState?: OperationState; - messageIsSuccess?: string; - messageIsFailure?: string; - messageIsLoading?: string; - /** name of file */ - name?: string; -} - interface IPanel { /** whether it's loading file, is completed, is failure */ operationState: OperationState; @@ -54,45 +34,28 @@ interface IInformativePanels { startWorkers: string[]; } -export interface IDropAreaProps - extends MainInterface, - React.HTMLAttributes { - isDragEnter?: boolean; - message?: string; -} - export interface IFileUploadV2Props extends MainInterface, React.HTMLAttributes { - /** whether or not the file upload component is disabled */ - isDisabled?: boolean; - /** component to render the drop area */ - DropArea: React.FC; - dropAreaProps?: IDropAreaProps; - /** component to render the informative panel */ - Panel: React.FC; /** 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 */ - processFile?: (base64String: string) => void; + 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 = ({ - isDisabled = false, - DropArea, - dropAreaProps = { padding: '10px' }, - Panel, isTestIsFailure = false, - processFile = (base64String: string) => null, + onFile = (base64String: string) => null, messageDuration = MESSAGE_DURATION, - padding = '10px', + dropAreaProps={}, ...props }): React.ReactElement => { const isMounted = useMounted(); @@ -102,9 +65,8 @@ export const FileUploadV2: React.FC = ({ makeItDisappear: [], startWorkers: [], }); - const [isDragEnter, setIsDragEnter] = useState(false); - const dropAreaRef = useRef(null); - const [width, setWidth] = useState(0); + const [dropAreaWidth, setDropAreaWidth] = useState(); + const dropAreaRef=useRef(null); /** * set end state @@ -154,7 +116,7 @@ export const FileUploadV2: React.FC = ({ informativePanel, ); } else { - processFile(base64StringFile); + onFile(base64StringFile); prepareForEndInformativePanel( OperationState.isSuccess, informativePanel, @@ -165,19 +127,11 @@ export const FileUploadV2: React.FC = ({ [ informativePanels.panels, isTestIsFailure, - processFile, + onFile, prepareForEndInformativePanel, ], ); - const onDragEnter = () => { - setIsDragEnter(true); - }; - - const onDragLeave = () => { - setIsDragEnter(false); - }; - /** * load array of informative panels and send order to start workers */ @@ -197,7 +151,6 @@ export const FileUploadV2: React.FC = ({ panels: [...prev.panels, ...newInformativePanels], startWorkers: [...fileNames], })); - setIsDragEnter(false); }, []); // start workers after files have been droped and array of informative panels @@ -247,13 +200,6 @@ export const FileUploadV2: React.FC = ({ } }, [informativePanels.makeItDisappear.length]); - const { getRootProps, getInputProps } = useDropzone({ - onDrop, - onDragEnter, - onDragLeave, - disabled: isDisabled, - }); - const onCancelUploading = (name: string) => () => { setInformativePanels((prev) => ({ ...prev, @@ -267,13 +213,6 @@ export const FileUploadV2: React.FC = ({ })); }; - // after first render, we compute width of the drop area component - useEffect(() => { - if (dropAreaRef.current) { - setWidth(dropAreaRef.current.getBoundingClientRect().width); - } - }, []); - // terminate workers on clean up function useEffect( () => () => { @@ -286,27 +225,23 @@ export const FileUploadV2: React.FC = ({ [informativePanels.panels], ); + useEffect(()=>{ + if(dropAreaRef.current){ + setDropAreaWidth(dropAreaRef.current.getBoundingClientRect().width); + } + },[]) + return ( - - - {() => ( -
- - -
- )} -
+ + {informativePanels.panels.map((panel) => ( - Cancel} name={panel.name} operationState={panel.operationState} + margin="10px 0" + style={{width:dropAreaWidth,boxSizing:'border-box'}} /> ))} @@ -314,10 +249,8 @@ export const FileUploadV2: React.FC = ({ }; const FileUploadV2Container = styled.div` - background-color: white; - border-radius: 10px; + background-color: ${({theme})=>theme.colors.background}; + border-radius: ${({theme})=>theme.dimensions.radius}; width: fit-content; - ${({ ...props }): string => ` - ${Main({ ...props })} - `} + ${({theme,...props }): string => Main({ padding:theme.dimensions.padding.container,...props })} `; diff --git a/src/Containers/index.ts b/src/Containers/index.ts index 77225ceb4..0083d9c60 100644 --- a/src/Containers/index.ts +++ b/src/Containers/index.ts @@ -99,3 +99,4 @@ export * from './StoreSelector/StoreSelector'; export * from './ScreenFlashEffect/ScreenFlashEffect'; export * from './PanelCard/PanelCard'; export * from './DropArea/DropArea'; +export * from './FileUploadV2/FileUploadV2'; From 5f7ce0b8012cfffcb4f39d7503e17a28ad33da2f Mon Sep 17 00:00:00 2001 From: roggc Date: Sun, 23 Jan 2022 02:58:52 +0100 Subject: [PATCH 6/6] extract logic into hooks --- src/Containers/FileUploadV2/FileUploadV2.tsx | 244 +++--------------- src/Containers/FileUploadV2/useGetWidth.ts | 20 ++ .../FileUploadV2/useInformativePanels.ts | 204 +++++++++++++++ 3 files changed, 254 insertions(+), 214 deletions(-) create mode 100644 src/Containers/FileUploadV2/useGetWidth.ts create mode 100644 src/Containers/FileUploadV2/useInformativePanels.ts diff --git a/src/Containers/FileUploadV2/FileUploadV2.tsx b/src/Containers/FileUploadV2/FileUploadV2.tsx index 74bf1fe5b..c4861cb40 100644 --- a/src/Containers/FileUploadV2/FileUploadV2.tsx +++ b/src/Containers/FileUploadV2/FileUploadV2.tsx @@ -1,38 +1,11 @@ -import React, { useState, useCallback, useEffect,useRef } from 'react'; +import React 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[]; -} +import { Button } from '@Inputs/Button/Button'; +import { PanelCard } from '../PanelCard/PanelCard'; +import { DropArea, IDropAreaProps } from '../DropArea/DropArea'; +import { useInformativePanels } from './useInformativePanels'; +import { useGetWidth } from './useGetWidth'; export interface IFileUploadV2Props extends MainInterface, @@ -46,7 +19,7 @@ export interface IFileUploadV2Props 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; + dropAreaProps?: IDropAreaProps; } /** * multiple file upload, in parallel, version 2 @@ -54,194 +27,36 @@ export interface IFileUploadV2Props export const FileUploadV2: React.FC = ({ isTestIsFailure = false, onFile = (base64String: string) => null, - messageDuration = MESSAGE_DURATION, - dropAreaProps={}, + messageDuration = 1500, + dropAreaProps = {}, ...props }): React.ReactElement => { - const isMounted = useMounted(); - const [informativePanels, setInformativePanels] = - useState({ - panels: [], - makeItDisappear: [], - startWorkers: [], - }); - const [dropAreaWidth, setDropAreaWidth] = useState(); - const dropAreaRef=useRef(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) => { - 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) => { - 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], + const [panels, onDrop, onCancelUploading] = useInformativePanels( + isTestIsFailure, + onFile, + messageDuration, ); - - useEffect(()=>{ - if(dropAreaRef.current){ - setDropAreaWidth(dropAreaRef.current.getBoundingClientRect().width); - } - },[]) + const [dropAreaWidth, dropAreaRef] = useGetWidth(); return ( - - {informativePanels.panels.map((panel) => ( + + {panels.map((panel) => ( Cancel} + cancelButtonOnLoading={ + + } name={panel.name} operationState={panel.operationState} margin="10px 0" - style={{width:dropAreaWidth,boxSizing:'border-box'}} + style={{ width: dropAreaWidth, boxSizing: 'border-box' }} /> ))} @@ -249,8 +64,9 @@ export const FileUploadV2: React.FC = ({ }; const FileUploadV2Container = styled.div` - background-color: ${({theme})=>theme.colors.background}; - border-radius: ${({theme})=>theme.dimensions.radius}; + 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 })} + ${({ theme, ...props }): string => + Main({ padding: theme.dimensions.padding.container, ...props })} `; diff --git a/src/Containers/FileUploadV2/useGetWidth.ts b/src/Containers/FileUploadV2/useGetWidth.ts new file mode 100644 index 000000000..fe1824ff1 --- /dev/null +++ b/src/Containers/FileUploadV2/useGetWidth.ts @@ -0,0 +1,20 @@ +import { useState, useRef, useEffect } from 'react'; + +/** + * gets the width of a component which extends HTMLDivElement props + */ +export const useGetWidth = (): readonly [ + number | undefined, + React.RefObject, +] => { + const [width, setWidth] = useState(); + const ref = useRef(null); + + useEffect(() => { + if (ref.current) { + setWidth(ref.current.getBoundingClientRect().width); + } + }, []); + + return [width, ref] as const; +}; diff --git a/src/Containers/FileUploadV2/useInformativePanels.ts b/src/Containers/FileUploadV2/useInformativePanels.ts new file mode 100644 index 000000000..9a2af4e1c --- /dev/null +++ b/src/Containers/FileUploadV2/useInformativePanels.ts @@ -0,0 +1,204 @@ +import {useState,useCallback,useEffect} from 'react' +import { useMounted } from '@Utils/Hooks'; +import {OperationState} from '../PanelCard/PanelCard'; +// @ts-ignore +import worker from 'workerize-loader!./worker'; // eslint-disable-line + +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 const useInformativePanels = ( + isTestIsFailure: boolean, + onFile: (base64StringFile: string) => void, + messageDuration: number, +): readonly [ + IPanel[], + (acceptedFiles: File[]) => void, + (name: string) => () => void, +] => { + const isMounted = useMounted(); + const [informativePanels, setInformativePanels] = + useState({ + panels: [], + makeItDisappear: [], + startWorkers: [], + }); + + /** + * 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, + ], + ); + + // start workers after files have been droped and array of informative panels + // are loaded + useEffect(() => { + if (informativePanels.startWorkers.length) { + informativePanels.startWorkers.forEach((name) => { + 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) => { + 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]); + + // terminate workers on clean up function + useEffect( + () => () => { + if (!isMounted.current) { + informativePanels.panels.forEach((panel) => + panel.worker?.terminate(), + ); + } + }, + [informativePanels.panels], + ); + + /** + * 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], + })); + }, []); + + const onCancelUploading = (name: string) => () => { + setInformativePanels((prev) => ({ + ...prev, + panels: prev.panels.filter((panel) => { + if (panel.name === name) { + panel.worker?.terminate(); + return false; + } + return true; + }), + })); + }; + + return [informativePanels.panels, onDrop, onCancelUploading] as const; +};