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 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
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
};
72 changes: 72 additions & 0 deletions src/Containers/FileUploadV2/FileUploadV2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import styled from 'styled-components';
import { MainInterface, Main } from '@Utils/BaseStyles';
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,
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 = 1500,
dropAreaProps = {},
...props
}): React.ReactElement => {
const [panels, onDrop, onCancelUploading] = useInformativePanels(
isTestIsFailure,
onFile,
messageDuration,
);
const [dropAreaWidth, dropAreaRef] = useGetWidth();

return (
<FileUploadV2Container {...props}>
<DropArea
onDropHandler={onDrop}
{...dropAreaProps}
ref={dropAreaRef}
/>
{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' }}
/>
))}
</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 })}
`;
20 changes: 20 additions & 0 deletions src/Containers/FileUploadV2/useGetWidth.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>,
] => {
const [width, setWidth] = useState<number | undefined>();
const ref = useRef<HTMLDivElement>(null);

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

return [width, ref] as const;
};
204 changes: 204 additions & 0 deletions src/Containers/FileUploadV2/useInformativePanels.ts
Original file line number Diff line number Diff line change
@@ -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<IInformativePanels>({
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;
};
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';