diff --git a/src/Containers/FileUpload/FileUpload.stories.tsx b/src/Containers/FileUpload/FileUpload.stories.tsx index 0c7a37ef8..3c3875f13 100644 --- a/src/Containers/FileUpload/FileUpload.stories.tsx +++ b/src/Containers/FileUpload/FileUpload.stories.tsx @@ -1,46 +1,47 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Meta, Story } from '@storybook/react'; import { FileUpload, IFileUploadProps } from '../../index'; - export default { title: 'Components/FileUpload', component: FileUpload, args: { - subTitle: 'Supports: JPG, JPEG2000, PNG', - title: 'Drop your image here, or click to browse', - minHeight: 100, - setBase64: (base64StringFile: string) => { + doWithBase64StringFile: (base64StringFile: string) => { console.log(base64StringFile); }, - successMessage: 'Completed', - failureMessage: 'Something went wrong', - disabled: false, }, } as Meta; -const useStateProps = () => { - const [isUploading, setIsUploading] = useState(false); - const [isSuccess, setIsSuccess] = useState(false); - const [isFailure, setIsFailure] = useState(false); - return { - isFailure, - isSuccess, - isUploading, - setIsFailure, - setIsSuccess, - setIsUploading, - }; -}; - export const Basic: Story = (args) => ( - + ); -export const VeryLongMessageDuration: Story = (args) => ( - -); +export const LongMessageDuration = Basic.bind({}); +LongMessageDuration.args = { + ...Basic.args, + messageDuration: 10000, +}; -export const VeryShortMessageDuration: Story = (args) => ( - -); +export const BigMinHeight = Basic.bind({}); +BigMinHeight.args = { + ...Basic.args, + minHeight: 350, +}; + +export const BigMinWidth = Basic.bind({}); +BigMinWidth.args = { + ...Basic.args, + minWidth: 600, +}; + +export const TestIsFailureTrue = Basic.bind({}); +TestIsFailureTrue.args = { + ...Basic.args, + isTestIsFailure: true, +}; + +export const IsDisabled = Basic.bind({}); +IsDisabled.args = { + ...Basic.args, + isDisabled: true, +}; diff --git a/src/Containers/FileUpload/FileUpload.tsx b/src/Containers/FileUpload/FileUpload.tsx index ff82a065e..6d697765d 100644 --- a/src/Containers/FileUpload/FileUpload.tsx +++ b/src/Containers/FileUpload/FileUpload.tsx @@ -9,7 +9,7 @@ import React, { import styled from 'styled-components'; import { flex } from '@Utils/Mixins'; import { TextLayout } from '@Layouts'; -import { useDropzone } from 'react-dropzone'; +import Dropzone, { useDropzone } from 'react-dropzone'; import { Image } from '@styled-icons/fa-solid/Image'; import { CheckCircle } from '@styled-icons/fa-solid/CheckCircle'; import { TimesCircle } from '@styled-icons/fa-solid/TimesCircle'; @@ -21,308 +21,135 @@ import { Loading } from '../Loading/Loading'; import { BottomPanel } from './BottomPanel'; import { Container, Icon } from './StyledComponents'; import { FileMovingAnimation } from './FileMovingAnimation'; -import { IsFailureIsSuccessPanel } from './IsFailureIsSuccessPanel'; +import { + IsFailureIsSuccessPanel, + IIsFailureIsSuccessPanelProps, +} from './IsFailureIsSuccessPanel'; import { NO_BASE64STRINGFILE } from './constants'; +import reducer, { + IFileUploadState, + SET_COMPONENT_WIDTH, + SET_HEIGHT, + SET_INITIAL_HEIGHT_PLUS_VALUES, + SET_INITIAL_HEIGHT_VALUES, + SET_IS_DRAG_ENTER, + SET_IS_SUCCESS_WIDTH, + SET_LOADING_CONTAINER_HEIGHT, + SET_OPACITY_IS_FAILURE, + SET_OPACITY_IS_SUCCESS, + SET_OPACITY_LOADING, + SET_POSITION_TOP_LOADING, + SET_TOTAL_HEIGHT_PLUS, + LOADING_RESTORE, + IS_FAILURE_RESTORE, + IS_SUCCESS_RESTORE, + ADD_LOADING_IS_SUCCESS_IS_FAILURE, + RESET_LOADING_IS_SUCCESS_IS_FAILURE, + REMOVE_LOADING_IS_SUCCESS_IS_FAILURE, +} from './reducer'; -interface IOptions { - position: boolean; - opacity: number; -} - -interface IState { - height: number | undefined; - totalHeight: number; - totalHeightPlus: number; - maxHeight: number | undefined; - loading: IOptions; - isSuccess: IOptions; - isFailure: IOptions; - positionTopLoading: number; - loadingContainerHeight: number; - componentWidth: number; - isSuccessWidth: number; - isDragEnter: boolean; -} +// 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 MAX_HEIGHT = 9000; -const TRANSITION_HEIGHT_ANIMATION_DURATION = 500; -const FIRST_FILE = 0; +const SUCCESS_MESSAGE = 'Completed'; +const FAILURE_MESSAGE = 'Something went wrong'; +const TITLE = 'Drop your files here, or click to browse'; +const SUBTITLE = 'Admits any kind of files'; +const TRANSITION_HEIGHT_ANIMATION_DURATION = 200; const PADDING = 10; const MARGIN = 10; -const SET_HEIGHT = 'SET_HEIGHT'; -const SET_MAX_HEIGHT = 'SET_MAX_HEIGHT'; -const SET_TOTAL_HEIGHT = 'SET_TOTAL_HEIGHT'; -const SET_COMPONENT_WIDTH = 'SET_COMPONENT_WIDTH'; -const SET_TOTAL_HEIGHT_PLUS = 'SET_TOTAL_HEIGHT_PLUS'; -const SET_POSITION_TOP_LOADING = 'SET_POSITION_TOP_LOADING'; -const SET_IS_SUCCESS_WIDTH = 'SET_IS_SUCCESS_WIDTH'; -const SET_OPACITY_LOADING = 'SET_OPACITY_LOADING'; -const SET_POSITION_LOADING = 'SET_POSITION_LOADING'; -const SET_OPACITY_IS_SUCCESS = 'SET_OPACITY_IS_SUCCESS'; -const SET_POSITION_IS_SUCCESS = 'SET_POSITION_IS_SUCCESS'; -const SET_OPACITY_IS_FAILURE = 'SET_OPACITY_IS_FAILURE'; -const SET_POSITION_IS_FAILURE = 'SET_POSITION_IS_FAILURE'; -const SET_LOADING_CONTAINER_HEIGHT = 'SET_LOADING_CONTAINER_HEIGHT'; -const SET_IS_DRAG_ENTER = 'SET_IS_DRAG_ENTER'; -const LOADING_FADE_OUT = 'LOADING_FADE_OUT'; -const LOADING_RESTORE = 'LOADING_RESTORE'; -const IS_SUCCESS_FADE_OUT = 'IS_SUCCESS_FADE_OUT'; -const IS_SUCCESS_RESTORE = 'IS_SUCCESS_RESTORE'; -const IS_FAILURE_FADE_OUT = 'IS_FAILURE_FADE_OUT'; -const IS_FAILURE_RESTORE = 'IS_FAILURE_RESTORE'; -const SET_INITIAL_HEIGHT_VALUES = 'SET_INITIAL_HEIGHT_VALUES'; -const SET_INITIAL_HEIGHT_PLUS_VALUES = 'SET_INITIAL_HEIGHT_PLUS_VALUES'; +interface ISpecificValues { + /** value of truth for the state of the panel; example: is success is true or false */ + value: boolean; + /** opacity of the panel, used for animation */ + opacity: number; + /** when true panel is positioned absolute */ + isPosition: boolean; +} -type Action = - | { - type: 'SET_HEIGHT' | 'SET_MAX_HEIGHT'; - value: number | undefined; - } - | { - type: - | 'SET_TOTAL_HEIGHT' - | 'SET_COMPONENT_WIDTH' - | 'SET_TOTAL_HEIGHT_PLUS' - | 'SET_POSITION_TOP_LOADING' - | 'SET_IS_SUCCESS_WIDTH' - | 'SET_OPACITY_LOADING' - | 'SET_OPACITY_IS_SUCCESS' - | 'SET_OPACITY_IS_FAILURE' - | 'SET_LOADING_CONTAINER_HEIGHT' - | 'SET_INITIAL_HEIGHT_VALUES'; - value: number; - } - | { - type: - | 'SET_POSITION_LOADING' - | 'SET_POSITION_IS_SUCCESS' - | 'SET_POSITION_IS_FAILURE' - | 'SET_IS_DRAG_ENTER'; - value: boolean; - } - | { - type: - | 'LOADING_FADE_OUT' - | 'LOADING_RESTORE' - | 'IS_SUCCESS_FADE_OUT' - | 'IS_SUCCESS_RESTORE' - | 'IS_FAILURE_FADE_OUT' - | 'IS_FAILURE_RESTORE' - | 'SET_INITIAL_HEIGHT_PLUS_VALUES'; - }; +interface IValue { + /** is success state for the panel */ + isSuccess: ISpecificValues; + /** is failure state for the panel */ + isFailure: ISpecificValues; + /** is uploading state for the panel */ + isUploading: ISpecificValues; + /** name of file associated with the informative panel */ + name: string; + /** worker; will do the job of reading the file */ + worker: Worker; + /** the file associated with the informative panel */ + file: File; +} -const reducer = (state: IState, action: Action): IState => { - switch (action.type) { - case SET_HEIGHT: - return { - ...state, - height: action.value, - }; - case SET_MAX_HEIGHT: - return { - ...state, - maxHeight: action.value, - }; - case SET_TOTAL_HEIGHT: - return { - ...state, - totalHeight: action.value, - }; - case SET_COMPONENT_WIDTH: - return { - ...state, - componentWidth: action.value, - }; - case SET_TOTAL_HEIGHT_PLUS: - return { - ...state, - totalHeightPlus: action.value, - }; - case SET_POSITION_TOP_LOADING: - return { - ...state, - positionTopLoading: action.value, - }; - case SET_IS_SUCCESS_WIDTH: - return { - ...state, - isSuccessWidth: action.value, - }; - case SET_OPACITY_LOADING: - return { - ...state, - loading: { - ...state.loading, - opacity: action.value, - }, - }; - case SET_POSITION_LOADING: - return { - ...state, - loading: { - ...state.loading, - position: action.value, - }, - }; - case SET_OPACITY_IS_SUCCESS: - return { - ...state, - isSuccess: { - ...state.isSuccess, - opacity: action.value, - }, - }; - case SET_POSITION_IS_SUCCESS: - return { - ...state, - isSuccess: { - ...state.isSuccess, - position: action.value, - }, - }; - case SET_OPACITY_IS_FAILURE: - return { - ...state, - isFailure: { - ...state.isFailure, - opacity: action.value, - }, - }; - case SET_POSITION_IS_FAILURE: - return { - ...state, - isFailure: { - ...state.isFailure, - position: action.value, - }, - }; - case SET_LOADING_CONTAINER_HEIGHT: - return { - ...state, - loadingContainerHeight: action.value, - }; - case SET_IS_DRAG_ENTER: - return { - ...state, - isDragEnter: action.value, - }; - case LOADING_FADE_OUT: - return { - ...state, - loading: { - ...state.loading, - opacity: 0, - position: true, - }, - }; - case LOADING_RESTORE: - return { - ...state, - loading: { - ...state.loading, - opacity: 1, - position: false, - }, - }; - case IS_SUCCESS_FADE_OUT: - return { - ...state, - isSuccess: { - ...state.isSuccess, - opacity: 0, - position: true, - }, - }; - case IS_SUCCESS_RESTORE: - return { - ...state, - isSuccess: { - ...state.isSuccess, - opacity: 1, - position: false, - }, - }; - case IS_FAILURE_FADE_OUT: - return { - ...state, - isFailure: { - ...state.isFailure, - opacity: 0, - position: true, - }, - }; - case IS_FAILURE_RESTORE: - return { - ...state, - isFailure: { - ...state.isFailure, - opacity: 1, - position: false, - }, - }; - case SET_INITIAL_HEIGHT_VALUES: - return { - ...state, - maxHeight: action.value, - totalHeight: action.value, - }; - case SET_INITIAL_HEIGHT_PLUS_VALUES: - return { - ...state, - height: undefined, - maxHeight: MAX_HEIGHT, - }; - default: - return state; - } -}; +interface IInformativePanelsState { + /** array of panels */ + values: IValue[]; + /** 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 IFileUploadProps { - title: string; - subTitle: string; - minHeight: number; + /** the title message; default value: 'Drop your files here, or click to browse' */ + title?: string; + /** the subtitle message; default value: 'Admits any kind of file' */ + subTitle?: string; + /** minimum height for the component; optional */ + minHeight?: number; + /** minimum width for the component; optional */ minWidth?: number; - setBase64: (base64StringFile: string) => void; - isUploading: boolean; - isSuccess: boolean; - isFailure: boolean; - successMessage: string; - failureMessage: string; - disabled: boolean; - setIsUploading: React.Dispatch>; - setIsSuccess: React.Dispatch>; - setIsFailure: React.Dispatch>; + /** + * function to process the file read and transformed to a base64 string; default: does nothing + * @param {string} base64StringFile the file read and transformed to a base64 string + */ + doWithBase64StringFile?: (base64StringFile: string) => void; + /** message of the bottom panel to inform of the success of the operation of reading the file content; default value: 'Completed' */ + successMessage?: string; + /** message of the bottom panel to inform of the failure of the operation of reading the file content; default value: 'Something went wrong' */ + failureMessage?: string; + /** if true, disables the component functionality; default value: false */ + isDisabled?: boolean; + /** time in ms of the presence of the bottom panel informing the result of the operation (sucess or failure); default value: 1500 */ messageDuration?: number; + /** 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; + /** height and fade in-fade out animation duration in ms; default value: 200 */ + heightAndOpacityTransitionDuration?: number; } +/** multiple file upload in parallel */ export const FileUpload: React.FC = ({ - title, - subTitle, + title = TITLE, + subTitle = SUBTITLE, minHeight, minWidth, - setBase64, - isUploading, - isSuccess, - isFailure, - successMessage, - failureMessage, - disabled, - setIsUploading, - setIsSuccess, - setIsFailure, + doWithBase64StringFile = (base64StringFile: string) => null, + successMessage = SUCCESS_MESSAGE, + failureMessage = FAILURE_MESSAGE, + isDisabled = false, messageDuration = MESSAGE_DURATION, + isTestIsFailure, + heightAndOpacityTransitionDuration = TRANSITION_HEIGHT_ANIMATION_DURATION, }): React.ReactElement => { - const initState: IState = { + const [informativePanelsState, setInformativePanelsState] = + useState({ + values: [], + makeItDisappear: [], + startWorkers: [], + }); + const initState: IFileUploadState = { height: undefined, totalHeight: 0, totalHeightPlus: 0, maxHeight: undefined, - loading: { position: !isUploading, opacity: 0 }, - isSuccess: { position: !isSuccess, opacity: 0 }, - isFailure: { position: !isFailure, opacity: 0 }, + loading: [], + isSuccess: [], + isFailure: [], positionTopLoading: 0, loadingContainerHeight: 0, componentWidth: 0, @@ -331,247 +158,389 @@ export const FileUpload: React.FC = ({ }; const [state, dispatch] = useReducer(reducer, initState); const isMounted = useMounted(); - const [fileName, setFileName] = useState(''); - - const workerRef = useRef(); - const base64StringFileRef = useRef(''); - const previousIsSuccessValue = useRef(isSuccess); - /** - * this is to execute setBase64 function after the animation finishes + * ref to the most outer container, which contains everything else */ - useEffect(() => { - if (previousIsSuccessValue.current) { - setTimeout(() => { - setBase64(base64StringFileRef.current); - }, TRANSITION_HEIGHT_ANIMATION_DURATION); - } - previousIsSuccessValue.current = isSuccess; - }, [isSuccess]); + const containerRef = useRef(null); + const loadingContainerRef = useRef(null); - // this is to calculate (set) some values after the first render - useEffect(() => { - if (containerRef.current?.scrollHeight) { - const innerContentHeight = - containerRef.current.scrollHeight - PADDING * 2; - dispatch({ - type: SET_INITIAL_HEIGHT_VALUES, - value: innerContentHeight, - }); - } - if (rootRef.current?.clientWidth) { - const innerComponentWidth = - rootRef.current.clientWidth - MARGIN * 2 - PADDING * 2; - dispatch({ type: SET_COMPONENT_WIDTH, value: innerComponentWidth }); - } + /** + * load array of informative panels (values) and send order to start workers + */ + const onDrop = useCallback((acceptedFiles: File[]) => { + const informativePanels = acceptedFiles.map((file) => { + const workerInstance = worker(); + return { + isSuccess: { value: false, isPosition: false, opacity: 1 }, + isFailure: { value: false, isPosition: false, opacity: 1 }, + isUploading: { value: true, isPosition: false, opacity: 1 }, + name: file.name, + worker: workerInstance, + file, + }; + }); + const fileNames = acceptedFiles.map((file) => file.name); + setInformativePanelsState((prev) => ({ + ...prev, + values: [...prev.values, ...informativePanels], + startWorkers: [...fileNames], + })); + dispatch({ type: SET_IS_DRAG_ENTER, value: false }); }, []); - useEffect(() => { - // this is to set some values the first time when the component it's expanded - if ( - state.height === undefined && - (isUploading || isSuccess || isFailure) - ) { - if (containerRef.current?.scrollHeight) { - dispatch({ - type: SET_TOTAL_HEIGHT_PLUS, - value: containerRef.current.scrollHeight - PADDING * 2, - }); - } - if (loadingContainerRef.current?.getBoundingClientRect().top) { - dispatch({ - type: SET_POSITION_TOP_LOADING, - value: - loadingContainerRef.current.getBoundingClientRect() - .top - MARGIN, - }); - } - } - // this is to calculate and set the width of the success and failure container component - if (isSuccess || isFailure) { - const width = containerRef.current?.getBoundingClientRect().width; - if (width) { - dispatch({ - type: SET_IS_SUCCESS_WIDTH, - value: width - PADDING * 2 - MARGIN * 2, - }); + /** + * 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; } - } - // this sets height of the component, is used to transition between heights. - if (isUploading || isSuccess || isFailure) { - if (state.totalHeightPlus) { - dispatch({ type: SET_HEIGHT, value: state.totalHeightPlus }); - } else { - dispatch({ type: SET_INITIAL_HEIGHT_PLUS_VALUES }); + const informativePanel = informativePanelsState.values.find( + (value) => value.name === name, + ); + if (informativePanel) { + if ( + base64StringFile === NO_BASE64STRINGFILE || + isTestIsFailure + ) { + setInformativePanelsState((prev) => ({ + ...prev, + values: prev.values.map((value) => { + if (value.name === informativePanel.name) + return { + ...value, + isSuccess: { + ...value.isSuccess, + value: false, + }, + isFailure: { + ...value.isFailure, + value: true, + }, + isUploading: { + ...value.isUploading, + value: false, + }, + }; + return value; + }), + makeItDisappear: [ + ...prev.makeItDisappear, + informativePanel.name, + ], + })); + } else { + doWithBase64StringFile(base64StringFile); + setInformativePanelsState((prev) => ({ + ...prev, + values: prev.values.map((value) => { + if (value.name === informativePanel.name) + return { + ...value, + isSuccess: { + ...value.isSuccess, + value: true, + }, + isFailure: { + ...value.isFailure, + value: false, + }, + isUploading: { + ...value.isUploading, + value: false, + }, + }; + return value; + }), + makeItDisappear: [ + ...prev.makeItDisappear, + informativePanel.name, + ], + })); + } } - } else if (state.totalHeight) { - dispatch({ type: SET_HEIGHT, value: state.totalHeight }); - } - // this is to calculate and set isuploading container panel. - if ( - isUploading && - !isFailure && - !isSuccess && - loadingContainerRef.current?.scrollHeight - ) { - const loadingContainerHeight = - loadingContainerRef.current.scrollHeight; - dispatch({ - type: SET_LOADING_CONTAINER_HEIGHT, - value: loadingContainerHeight, - }); - } - }, [ - state.height, - isUploading, - isSuccess, - isFailure, - state.totalHeight, - state.totalHeightPlus, - ]); - - useEffect(() => { - if (!isUploading) { - dispatch({ type: SET_OPACITY_LOADING, value: 0 }); - } - return () => { - dispatch({ type: LOADING_RESTORE }); - }; - }, [isUploading]); - - useEffect(() => { - if (!isFailure) { - dispatch({ type: SET_OPACITY_IS_FAILURE, value: 0 }); - } - return () => { - dispatch({ type: IS_FAILURE_RESTORE }); - }; - }, [isFailure]); + }, + [ + informativePanelsState.values, + isTestIsFailure, + doWithBase64StringFile, + ], + ); + // start workers after files have been droped and array of values (informative panels) + // are loaded useEffect(() => { - if (!isSuccess) { - dispatch({ type: SET_OPACITY_IS_SUCCESS, value: 0 }); - } - return () => { - dispatch({ type: IS_SUCCESS_RESTORE }); - }; - }, [isSuccess]); - - // this is used to resize bottom panel with when resizing window browser - useLayoutEffect(() => { - function updateSize() { - if (rootRef.current?.clientWidth) { - const innerComponentWidth = - rootRef.current.clientWidth - MARGIN * 2 - PADDING * 2; - dispatch({ - type: SET_COMPONENT_WIDTH, - value: innerComponentWidth, - }); - } - if (isSuccess || isFailure) { - const width = - containerRef.current?.getBoundingClientRect().width; - if (width) { - dispatch({ - type: SET_IS_SUCCESS_WIDTH, - value: width - PADDING * 2 - MARGIN * 2, + if (informativePanelsState.startWorkers.length) { + informativePanelsState.startWorkers.forEach((name) => { + const informativePanel = informativePanelsState.values.find( + (value) => value.name === name, + ); + if (informativePanel) { + informativePanel.worker.onmessage = onWorkerMessage; + informativePanel.worker.postMessage({ + file: informativePanel.file, }); } - } + }); + setInformativePanelsState((prev) => ({ + ...prev, + startWorkers: [], + })); } - window.addEventListener('resize', updateSize); - updateSize(); - return () => window.removeEventListener('resize', updateSize); - }, [isSuccess, isFailure]); + }, [informativePanelsState.startWorkers.length, onWorkerMessage]); - const containerRef = useRef(null); - const loadingContainerRef = useRef(null); + // make disappear values (informative panels) in the future + useEffect(() => { + if (informativePanelsState.makeItDisappear.length) { + informativePanelsState.makeItDisappear.forEach((name) => { + setTimeout(() => { + if (isMounted.current) { + setInformativePanelsState((prev) => ({ + ...prev, + values: prev.values.filter((value) => { + if (value.name === name) { + value.worker.terminate(); + return false; + } + return true; + }), + makeItDisappear: prev.makeItDisappear.filter( + (name_) => name_ !== name, + ), + })); + } + }, messageDuration); + }); + } + }, [informativePanelsState.makeItDisappear.length]); - const onDrop = useCallback((acceptedFiles: File[]) => { - setFileName(acceptedFiles[FIRST_FILE].name); - setIsUploading(true); - setIsSuccess(false); - setIsFailure(false); - acceptedFiles.forEach((file) => { - const workerInstance = worker(); - workerRef.current = workerInstance; - workerInstance.onmessage = (e: any) => { - const { base64StringFile } = e.data; - if (base64StringFile === NO_BASE64STRINGFILE) { - workerRef.current?.terminate(); - setIsFailure(true); - setIsSuccess(false); - setIsUploading(false); - setTimeout(() => { - if (isMounted.current) setIsFailure(false); - }, messageDuration); - } else if (base64StringFile !== undefined) { - workerRef.current?.terminate(); - base64StringFileRef.current = base64StringFile; - setIsSuccess(true); - setIsFailure(false); - setIsUploading(false); - setTimeout(() => { - if (isMounted.current) { - setIsSuccess(false); - } - }, messageDuration); - } - }; - workerInstance.postMessage({ file }); - dispatch({ type: SET_IS_DRAG_ENTER, value: false }); - }); - }, []); const onDragEnter = useCallback((event: React.DragEvent) => { event.preventDefault(); dispatch({ type: SET_IS_DRAG_ENTER, value: true }); }, []); + const onDragLeave = useCallback((event: React.DragEvent) => { event.preventDefault(); dispatch({ type: SET_IS_DRAG_ENTER, value: false }); }, []); + const { getRootProps, getInputProps, rootRef } = useDropzone({ onDrop, onDragEnter, onDragLeave, - disabled, + disabled: isDisabled, }); - const renderChild = (): React.ReactElement | undefined => { - if (isFailure) { - return ( - - ); - } - if (isSuccess) { - return ( - - ); - } - if (isUploading) { - return ( - - ); - } - return undefined; - }; + // calculate (set) some values after the first render + // useEffect(() => { + // if (containerRef.current?.scrollHeight) { + // const innerContentHeight = + // containerRef.current.scrollHeight - PADDING * 2; + // console.log('set initial height values'); + // dispatch({ + // type: SET_INITIAL_HEIGHT_VALUES, + // value: innerContentHeight, + // }); + // } + // if (rootRef.current?.clientWidth) { + // const innerComponentWidth = + // rootRef.current.clientWidth - MARGIN * 2 - PADDING * 2; + // dispatch({ type: SET_COMPONENT_WIDTH, value: innerComponentWidth }); + // } + // }, []); - const onCancelUploading = () => { - if (workerRef.current) { - workerRef.current.terminate(); - setIsUploading(false); - } + // useEffect(() => { + // // set some values the first time when the component it's expanded + // if ( + // state.height === undefined && + // (isUploading.reduce((acc,value)=>acc||value,false) || isSuccess.reduce((acc,value)=>acc||value,false) || isFailure.reduce((acc,value)=>acc||value,false)) + // ) { + // console.log('set total height plus') + // if (containerRef.current?.scrollHeight) { + // dispatch({ + // type: SET_TOTAL_HEIGHT_PLUS, + // value: containerRef.current.scrollHeight - PADDING * 2, + // }); + // } + // if (loadingContainerRef.current?.getBoundingClientRect().top) { + // dispatch({ + // type: SET_POSITION_TOP_LOADING, + // value: + // loadingContainerRef.current.getBoundingClientRect() + // .top - MARGIN, + // }); + // } + // } + // // calculate and set the width of the success and failure container component + // if (isSuccess.reduce((acc,value)=>acc||value,false) || isFailure.reduce((acc,value)=>acc||value,false) ) { + // const width = containerRef.current?.getBoundingClientRect().width; + // if (width) { + // dispatch({ + // type: SET_IS_SUCCESS_WIDTH, + // value: width - PADDING * 2 - MARGIN * 2, + // }); + // } + // } + // // sets height of the component, is used to transition between heights. + // if (isUploading.reduce((acc,value)=>acc||value,false) || isSuccess.reduce((acc,value)=>acc||value,false) || isFailure.reduce((acc,value)=>acc||value,false) ) { + // console.log('setting totalheightplus or initial height plus values'); + // if (state.totalHeightPlus) { + // dispatch({ type: SET_HEIGHT, value: state.totalHeightPlus }); + // } else { + // dispatch({ type: SET_INITIAL_HEIGHT_PLUS_VALUES }); + // } + // } else if (state.totalHeight) { + // dispatch({ type: SET_HEIGHT, value: state.totalHeight }); + // } + // // calculate and set isUploading container panel. + // if ( + // isUploading.reduce((acc,value)=>acc||value,false) && + // !isFailure.reduce((acc,value)=>acc||value,false) && + // !isSuccess.reduce((acc,value)=>acc||value,false) && + // loadingContainerRef.current?.scrollHeight + // ) { + // console.log('setting loading container height'); + // const loadingContainerHeight = + // loadingContainerRef.current.scrollHeight; + // dispatch({ + // type: SET_LOADING_CONTAINER_HEIGHT, + // value: loadingContainerHeight, + // }); + // } + // }, [ + // state.height, + // isUploading, + // isSuccess, + // isFailure, + // state.totalHeight, + // state.totalHeightPlus, + // ]); + + // useEffect(() => { + // isUploading.forEach((value,index)=>{ + // if(!value){ + // dispatch({ type: SET_OPACITY_LOADING, value: 0,index }); + // } + // }) + // return () => { + // isUploading.forEach((_,index)=>{ + // dispatch({ type: LOADING_RESTORE,index }); + // }) + // }; + // }, [isUploading]); + + // useEffect(() => { + // isFailure.forEach((value,index)=>{ + // if(!value){ + // dispatch({ type: SET_OPACITY_IS_FAILURE, value: 0,index }); + // } + // }) + // return () => { + // isFailure.forEach((_,index)=>{ + // dispatch({ type: IS_FAILURE_RESTORE,index }); + // }) + // }; + // }, [isFailure]); + + // useEffect(() => { + // isSuccess.forEach((value,index)=>{ + // if(!value){ + // dispatch({ type: SET_OPACITY_IS_SUCCESS, value: 0,index }); + // } + // }) + // return () => { + // isFailure.forEach((_,index)=>{ + // dispatch({ type: IS_SUCCESS_RESTORE,index }); + // }) + // }; + // }, [isSuccess]); + + // resize bottom panel width when resizing window browser + // useLayoutEffect(() => { + // function updateSize() { + // if (rootRef.current?.clientWidth) { + // const innerComponentWidth = + // rootRef.current.clientWidth - MARGIN * 2 - PADDING * 2; + // dispatch({ + // type: SET_COMPONENT_WIDTH, + // value: innerComponentWidth, + // }); + // } + // if (isSuccess || isFailure) { + // const width = + // containerRef.current?.getBoundingClientRect().width; + // if (width) { + // dispatch({ + // type: SET_IS_SUCCESS_WIDTH, + // value: width - PADDING * 2 - MARGIN * 2, + // }); + // } + // } + // } + // window.addEventListener('resize', updateSize); + // updateSize(); + // return () => window.removeEventListener('resize', updateSize); + // }, [isSuccess, isFailure]); + + /** renders isSuccess panel, or isFailure panel, or isUploading panel, depending + * on the state of the informative panel + */ + const renderBottomPanelContent = useCallback( + (value: IValue): React.ReactElement | undefined => { + let isFailureIsSuccessPanelProps: IIsFailureIsSuccessPanelProps | null = + null; + if (value.isFailure.value) { + isFailureIsSuccessPanelProps = { + message: failureMessage, + iconColor: MainTheme.colors.statusColors.red, + IconToShow: TimesCircle, + heightAndOpacityTransitionDuration, + }; + } + if (value.isSuccess.value) { + isFailureIsSuccessPanelProps = { + message: successMessage, + iconColor: MainTheme.colors.statusColors.green, + IconToShow: CheckCircle, + heightAndOpacityTransitionDuration, + }; + } + if (isFailureIsSuccessPanelProps) { + return ( + + ); + } + if (value.isUploading.value) { + return ( + + ); + } + return undefined; + }, + [], + ); + + /** cancel uploading; terminate worker and remove informative panel */ + const onCancelUploading = (informativePanel: IValue) => () => { + setInformativePanelsState((prev) => ({ + ...prev, + values: prev.values.filter((value) => { + if (value.name === informativePanel.name) { + value.worker.terminate(); + return false; + } + return true; + }), + })); }; return ( @@ -584,71 +553,92 @@ export const FileUpload: React.FC = ({ overflow="hidden" height={state.height} margin={`${MARGIN}px`} + isWidthFitContent + heightAndOpacityTransitionDuration={heightAndOpacityTransitionDuration} > - - + {() => ( + + + {state.isDragEnter ? ( + + ) : ( + + )} + + {title} + + + {subTitle} + + + + + )} + + {informativePanelsState.values.map((value, index) => ( + - {state.isDragEnter ? ( - - ) : ( - - )} - - {title} - - - {subTitle} - - - - - - {renderChild()} - + {renderBottomPanelContent(value)} + + ))} ); }; diff --git a/src/Containers/FileUpload/IsFailureIsSuccessPanel.tsx b/src/Containers/FileUpload/IsFailureIsSuccessPanel.tsx index 502268ebf..5a1d0ff0d 100644 --- a/src/Containers/FileUpload/IsFailureIsSuccessPanel.tsx +++ b/src/Containers/FileUpload/IsFailureIsSuccessPanel.tsx @@ -1,16 +1,16 @@ import React, { useState, useEffect } from 'react'; import { TextLayout } from '@Layouts'; import { StyledIcon } from '@styled-icons/styled-icon'; -import { Container, Icon } from './StyledComponents'; +import { Container, Icon, IContainerProps } from './StyledComponents'; -interface IIsFailureIsSuccessPanelProps { +export interface IIsFailureIsSuccessPanelProps extends IContainerProps{ IconToShow: StyledIcon; iconColor: string; message: string; } export const IsFailureIsSuccessPanel: React.FC = - ({ IconToShow, iconColor, message }): React.ReactElement => { + ({ IconToShow, iconColor, message,...props }): React.ReactElement => { const [opacity, setOpacity] = useState(0); useEffect(() => { setOpacity(1); @@ -21,6 +21,7 @@ export const IsFailureIsSuccessPanel: React.FC = flexGrow margin="10px" opacity={opacity} + {...props} > {message} diff --git a/src/Containers/FileUpload/StyledComponents.ts b/src/Containers/FileUpload/StyledComponents.ts index 55e93f9b9..e7e2b9879 100644 --- a/src/Containers/FileUpload/StyledComponents.ts +++ b/src/Containers/FileUpload/StyledComponents.ts @@ -7,6 +7,7 @@ export interface IContainerProps { withFlexSpaceBetween?: boolean; withBorder?: boolean; width?: number; + isWidthFitContent?:boolean; padding?: string; isDragEnter?: boolean; backgroundColor?: string; @@ -19,6 +20,8 @@ export interface IContainerProps { margin?: string; positionTop?: number; flexGrow?: boolean; + /** transition duration in ms; applied on height and opacity properties; */ + heightAndOpacityTransitionDuration:number; } export const Container = styled.div` @@ -27,6 +30,7 @@ export const Container = styled.div` withFlexCenter, withBorder, width, + isWidthFitContent, padding, isDragEnter, withFlexSpaceBetween, @@ -40,6 +44,7 @@ export const Container = styled.div` margin, positionTop, flexGrow, + heightAndOpacityTransitionDuration, }): string => ` ${borderRadius ? `border-radius:${borderRadius};` : 'border-radius:10px;'} ${ @@ -52,6 +57,7 @@ border:2px ${dashed ? 'dashed' : 'solid'} rgba(128,128,128,.8); ${withFlexCenter ? flex('center') : ''} ${withFlexSpaceBetween ? flex('space-between', 'center') : ''} ${width ? `width:${width}px;` : ''} +${isWidthFitContent ? `width:fit-content;` : ''} ${padding ? `padding:${padding};` : ''} ${ isDragEnter @@ -81,8 +87,10 @@ ${overflow ? `overflow:${overflow};` : ''} ${position ? `position:absolute;top:${positionTop}px;` : ''} ${margin ? `margin:${margin};` : ''} ${flexGrow ? `flex:1;` : ''} + +transition:height ${heightAndOpacityTransitionDuration}ms,opacity ${heightAndOpacityTransitionDuration}ms,max-height ${heightAndOpacityTransitionDuration*10}ms; `} - transition:height .5s,opacity .5s,max-height 5s; + `; interface IIconProps { diff --git a/src/Containers/FileUpload/reducer.ts b/src/Containers/FileUpload/reducer.ts new file mode 100644 index 000000000..d083731d1 --- /dev/null +++ b/src/Containers/FileUpload/reducer.ts @@ -0,0 +1,339 @@ +const MAX_HEIGHT = 9000; + +interface IOptions { + position: boolean; + opacity: number; +} + +export interface IFileUploadState { + height: number | undefined; + totalHeight: number; + totalHeightPlus: number; + maxHeight: number | undefined; + loading: IOptions[]; + isSuccess: IOptions[]; + isFailure: IOptions[]; + positionTopLoading: number; + loadingContainerHeight: number; + componentWidth: number; + isSuccessWidth: number; + isDragEnter: boolean; +} + +export const SET_HEIGHT = 'SET_HEIGHT'; +export const SET_MAX_HEIGHT = 'SET_MAX_HEIGHT'; +export const SET_TOTAL_HEIGHT = 'SET_TOTAL_HEIGHT'; +export const SET_COMPONENT_WIDTH = 'SET_COMPONENT_WIDTH'; +export const SET_TOTAL_HEIGHT_PLUS = 'SET_TOTAL_HEIGHT_PLUS'; +export const SET_POSITION_TOP_LOADING = 'SET_POSITION_TOP_LOADING'; +export const SET_IS_SUCCESS_WIDTH = 'SET_IS_SUCCESS_WIDTH'; +export const SET_OPACITY_LOADING = 'SET_OPACITY_LOADING'; +export const SET_POSITION_LOADING = 'SET_POSITION_LOADING'; +export const SET_OPACITY_IS_SUCCESS = 'SET_OPACITY_IS_SUCCESS'; +export const SET_POSITION_IS_SUCCESS = 'SET_POSITION_IS_SUCCESS'; +export const SET_OPACITY_IS_FAILURE = 'SET_OPACITY_IS_FAILURE'; +export const SET_POSITION_IS_FAILURE = 'SET_POSITION_IS_FAILURE'; +export const SET_LOADING_CONTAINER_HEIGHT = 'SET_LOADING_CONTAINER_HEIGHT'; +export const SET_IS_DRAG_ENTER = 'SET_IS_DRAG_ENTER'; +export const LOADING_FADE_OUT = 'LOADING_FADE_OUT'; +export const LOADING_RESTORE = 'LOADING_RESTORE'; +export const IS_SUCCESS_FADE_OUT = 'IS_SUCCESS_FADE_OUT'; +export const IS_SUCCESS_RESTORE = 'IS_SUCCESS_RESTORE'; +export const IS_FAILURE_FADE_OUT = 'IS_FAILURE_FADE_OUT'; +export const IS_FAILURE_RESTORE = 'IS_FAILURE_RESTORE'; +export const SET_INITIAL_HEIGHT_VALUES = 'SET_INITIAL_HEIGHT_VALUES'; +export const SET_INITIAL_HEIGHT_PLUS_VALUES = 'SET_INITIAL_HEIGHT_PLUS_VALUES'; +export const ADD_LOADING_IS_SUCCESS_IS_FAILURE='ADD_LOADING_IS_SUCCESS_IS_FAILURE'; +export const RESET_LOADING_IS_SUCCESS_IS_FAILURE='RESET_LOADING_IS_SUCCESS_IS_FAILURE' +export const REMOVE_LOADING_IS_SUCCESS_IS_FAILURE='REMOVE_LOADING_IS_SUCCESS_IS_FAILURE' + +type Action = + | { + type: 'SET_HEIGHT' | 'SET_MAX_HEIGHT'; + value: number | undefined; + } + | { + type: + | 'SET_TOTAL_HEIGHT' + | 'SET_COMPONENT_WIDTH' + | 'SET_TOTAL_HEIGHT_PLUS' + | 'SET_POSITION_TOP_LOADING' + | 'SET_IS_SUCCESS_WIDTH' + | 'SET_LOADING_CONTAINER_HEIGHT' + | 'SET_INITIAL_HEIGHT_VALUES'; + value: number; + } + | { + type: + | 'SET_OPACITY_LOADING' + | 'SET_OPACITY_IS_SUCCESS' + | 'SET_OPACITY_IS_FAILURE'; + value: number; + index: number; + } + | { + type: + | 'SET_POSITION_LOADING' + | 'SET_POSITION_IS_SUCCESS' + | 'SET_POSITION_IS_FAILURE'; + index: number; + value: boolean; + } + | { + type: 'SET_IS_DRAG_ENTER'; + value: boolean; + } + | { + type: + | 'LOADING_RESTORE' + | 'LOADING_FADE_OUT' + | 'IS_SUCCESS_FADE_OUT' + | 'IS_SUCCESS_RESTORE' + | 'IS_FAILURE_FADE_OUT' + | 'IS_FAILURE_RESTORE' + |'REMOVE_LOADING_IS_SUCCESS_IS_FAILURE'; + index: number; + } + | { + type: |'SET_INITIAL_HEIGHT_PLUS_VALUES'|'RESET_LOADING_IS_SUCCESS_IS_FAILURE'; + }|{ + type:|'ADD_LOADING_IS_SUCCESS_IS_FAILURE'; + value:IOptions; + }; + +const reducer = (state: IFileUploadState, action: Action): IFileUploadState => { + switch (action.type) { + case SET_HEIGHT: + return { + ...state, + height: action.value, + }; + case SET_MAX_HEIGHT: + return { + ...state, + maxHeight: action.value, + }; + case SET_TOTAL_HEIGHT: + return { + ...state, + totalHeight: action.value, + }; + case SET_COMPONENT_WIDTH: + return { + ...state, + componentWidth: action.value, + }; + case SET_TOTAL_HEIGHT_PLUS: + return { + ...state, + totalHeightPlus: action.value, + }; + case SET_POSITION_TOP_LOADING: + return { + ...state, + positionTopLoading: action.value, + }; + case SET_IS_SUCCESS_WIDTH: + return { + ...state, + isSuccessWidth: action.value, + }; + case SET_OPACITY_LOADING: + return { + ...state, + loading: state.loading.map((value, index) => { + if (index === action.index) + return { + ...state.loading[index], + opacity: action.value, + }; + return value; + }), + }; + case SET_POSITION_LOADING: + return { + ...state, + loading: state.loading.map((value, index) => { + if (index === action.index) + return { + ...value, + position: action.value, + }; + return value; + }), + }; + case SET_OPACITY_IS_SUCCESS: + return { + ...state, + isSuccess: state.isSuccess.map((value, index) => { + if (index === action.index) + return { + ...value, + opacity: action.value, + }; + return value; + }), + }; + case SET_POSITION_IS_SUCCESS: + return { + ...state, + isSuccess: state.isSuccess.map((value, index) => { + if (index === action.index) + return { + ...value, + position: action.value, + }; + return value; + }), + }; + case SET_OPACITY_IS_FAILURE: + return { + ...state, + isFailure: state.isFailure.map((value, index) => { + if (index === action.index) + return { + ...value, + opacity: action.value, + }; + return value; + }), + }; + case SET_POSITION_IS_FAILURE: + return { + ...state, + isFailure: state.isFailure.map((value, index) => { + if (index === action.index) + return { + ...value, + position: action.value, + }; + return value; + }), + }; + case SET_LOADING_CONTAINER_HEIGHT: + return { + ...state, + loadingContainerHeight: action.value, + }; + case SET_IS_DRAG_ENTER: + return { + ...state, + isDragEnter: action.value, + }; + case LOADING_FADE_OUT: + return { + ...state, + loading: state.loading.map((value, index) => { + if (index === action.index) + return { + ...value, + opacity: 0, + position: true, + }; + return value; + }), + }; + case LOADING_RESTORE: + return { + ...state, + loading: state.loading.map((value, index) => { + if (index === action.index) + return { + ...value, + opacity: 1, + position: false, + }; + return value; + }), + }; + case IS_SUCCESS_FADE_OUT: + return { + ...state, + isSuccess: state.isSuccess.map((value, index) => { + if (index === action.index) + return { + ...value, + opacity: 0, + position: true, + }; + return value; + }), + }; + case IS_SUCCESS_RESTORE: + return { + ...state, + isSuccess: state.isSuccess.map((value, index) => { + if (index === action.index) + return { + ...value, + opacity: 1, + position: false, + }; + return value; + }), + }; + case IS_FAILURE_FADE_OUT: + return { + ...state, + isFailure: state.isFailure.map((value, index) => { + if (index === action.index) + return { + ...value, + opacity: 0, + position: true, + }; + return value; + }), + }; + case IS_FAILURE_RESTORE: + return { + ...state, + isFailure: state.isFailure.map((value, index) => { + if (index === action.index) + return { + ...value, + opacity: 1, + position: false, + }; + return value; + }), + }; + case SET_INITIAL_HEIGHT_VALUES: + return { + ...state, + maxHeight: action.value, + totalHeight: action.value, + }; + case SET_INITIAL_HEIGHT_PLUS_VALUES: + return { + ...state, + height: undefined, + maxHeight: MAX_HEIGHT, + }; + case ADD_LOADING_IS_SUCCESS_IS_FAILURE: + return { + ...state, + loading:[...state.loading,{position:action.value.position,opacity:action.value.opacity}], + isSuccess:[...state.isSuccess,{position:action.value.position,opacity:action.value.opacity}], + isFailure:[...state.isFailure,{position:action.value.position,opacity:action.value.opacity}] + }; + case RESET_LOADING_IS_SUCCESS_IS_FAILURE: + return{ + ...state, + loading:[], + isSuccess:[], + isFailure:[], + }; + case REMOVE_LOADING_IS_SUCCESS_IS_FAILURE: + return { + ...state, + loading:state.loading.filter((_,index)=>index!==action.index), + isSuccess:state.isSuccess.filter((_,index)=>index!==action.index), + isFailure:state.isFailure.filter((_,index)=>index!==action.index) + } + default: + return state; + } +}; + +export default reducer; diff --git a/src/Containers/FileUpload/worker.js b/src/Containers/FileUpload/worker.js index 7265bc1de..371076778 100644 --- a/src/Containers/FileUpload/worker.js +++ b/src/Containers/FileUpload/worker.js @@ -15,7 +15,7 @@ onmessage = (e) => { ); } } - postMessage({ base64StringFile }); + postMessage({ base64StringFile,name:file.name }); }; reader.readAsArrayBuffer(file); }; diff --git a/src/Inputs/Button/Button.tsx b/src/Inputs/Button/Button.tsx index a8833d571..cc5b1e48a 100644 --- a/src/Inputs/Button/Button.tsx +++ b/src/Inputs/Button/Button.tsx @@ -55,7 +55,7 @@ export const Button: React.FC = ({ hasText={children} /> )} - {children && {children}} + {children && {children}} {isLoading && } ); @@ -140,10 +140,10 @@ const Icon = styled.svg` `} `; -const Content = styled.span<{ loading: boolean }>` +const Content = styled.span<{ isLoading: boolean }>` ${transition(['transform', 'opacity'])} - ${({ loading }): string => - loading + ${({ isLoading }): string => + isLoading ? ` transform: translate3d(0,80%,0); opacity: 0; diff --git a/src/Inputs/CustomSearch/CustomSearch.tsx b/src/Inputs/CustomSearch/CustomSearch.tsx index d8bdc21d5..491cc3d66 100644 --- a/src/Inputs/CustomSearch/CustomSearch.tsx +++ b/src/Inputs/CustomSearch/CustomSearch.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { StyledIcon } from 'styled-icons/types'; import { MainInterface, ResponsiveInterface } from '@Utils/BaseStyles'; -import { Card as C } from '../../Containers'; +import { Card as C } from '@Containers'; import { Button as B } from '../Button/Button'; import { Select } from '../Select/Select'; import { SearchBar } from '../SearchBar/SearchBar';