All Downloads are FREE. Search and download functionalities are using the official Maven repository.

components.controls.FileUploader.withFileUploader.jsx Maven / Gradle / Ivy

import React, { Component } from 'react'
import axios from 'axios'
import PropTypes from 'prop-types'
import isEmpty from 'lodash/isEmpty'
import isEqual from 'lodash/isEqual'
import isArray from 'lodash/isArray'
import reduce from 'lodash/reduce'
import every from 'lodash/every'
import some from 'lodash/some'
import get from 'lodash/get'
import isFunction from 'lodash/isFunction'
import has from 'lodash/has'
import find from 'lodash/find'

import { WithPropsResolver } from '../../../core/Expression/withResolver'
import { parseExpression } from '../../../core/Expression/parse'
import { id } from '../../../utils/id'

import { deleteFile, everyIsValid, post } from './utils'

/**
 * @type {Function} ReturnedComponent
 */
const FileUploaderControl = (WrappedComponent) => {
    class ReturnedComponent extends Component {
        constructor(props) {
            super(props)

            this.state = {
                files: props.files || [],
                imgFiles: [],
                imgError: {},
            }

            this.requests = {}

            this.handleDrop = this.handleDrop.bind(this)
            this.onDropRejected = this.onDropRejected.bind(this)
            this.handleImagesDrop = this.handleImagesDrop.bind(this)
            this.handleRemove = this.handleRemove.bind(this)
            this.handleChange = this.handleChange.bind(this)
            this.startUpload = this.startUpload.bind(this)
            this.onStartUpload = this.onStartUpload.bind(this)
            this.getUploaderProps = this.getUploaderProps.bind(this)
            this.resolveUrl = this.resolveUrl.bind(this)
            this.onDragEnter = this.onDragEnter.bind(this)
            this.onDragLeave = this.onDragLeave.bind(this)
            this.onError = this.onError.bind(this)
            this.clearState = this.clearState.bind(this)
        }

        componentDidMount() {
            const { mapper, value, model, fieldKey } = this.props
            const { files } = this.state

            this.setState({
                files: mapper
                    ? mapper(value)
                    : this.mapFiles(!isEmpty(value) ? value : files),
            })

            if (isEmpty(value) && model) {
                const files = model[fieldKey] || []

                this.setState({
                    files: Array.isArray(files)
                        ? this.mapFiles(files)
                        : this.mapFiles([files]),
                })
            }
        }

        componentDidUpdate(prevProps) {
            const { value, files, mapper } = this.props
            const { files: stateFiles } = this.state

            if (!isEqual(prevProps.value, value)) {
                if (!value) {
                    this.setState({ files: [] })
                }

                const newFiles = mapper
                    ? mapper(value || [])
                    : this.mapFiles(value || [])

                const hasUpdate = !every(newFiles, file => some(stateFiles, file))

                if (hasUpdate) {
                    this.setState({ files: newFiles })
                }
            } else if (!isEqual(prevProps.files, files)) {
                this.setState({
                    files: mapper ? mapper(files || []) : this.mapFiles(files || []),
                })
            }
        }

        clearState() {
            this.setState({ files: [] })
        }

        mapFiles(files) {
            if (!files) {
                return
            }

            const currentFiles = isArray(files) ? files : [files]

            // eslint-disable-next-line consistent-return
            return currentFiles.map(file => this.fileAdapter(file))
        }

        fileAdapter(file) {
            const {
                valueFieldId,
                labelFieldId,
                statusFieldId,
                sizeFieldId,
                responseFieldId,
                urlFieldId,
            } = this.props

            return {
                id: file[valueFieldId],
                name: file[labelFieldId] || file.fileName,
                status: file[statusFieldId],
                size: file[sizeFieldId],
                response: file[responseFieldId],
                link: file[urlFieldId],
            }
        }

        /**
         * Получение Url из expression
         * @returns {*}
         */
        resolveUrl(url) {
            if (!parseExpression(url)) {
                return url
            }

            const { propsResolver } = this.props

            // TODO разобраться что-за контекст такой тут ожидается
            const { _reduxForm } = this.context
            const { resolveModel } = _reduxForm

            return propsResolver(url, resolveModel)
        }

        /**
         * Return props
         */
        getUploaderProps() {
            const { files, imgFiles, uploading, uploaderClass, imgError } = this.state
            const { multi } = this.props

            return {
                ...this.props,
                files,
                imgFiles,
                uploading,
                requests: this.requests,
                multiple: multi,
                uploaderClass,
                onFocus: () => {},
                onBlur: () => {},
                onDrop: this.handleDrop,
                onDropRejected: this.onDropRejected,
                onImagesDrop: this.handleImagesDrop,
                onDragLeave: this.onDragLeave,
                onDragEnter: this.onDragEnter,
                onRemove: this.handleRemove,
                onStartUpload: this.onStartUpload,
                imgError,
            }
        }

        handleDrop(data) {
            const { onChange, autoUpload, onBlur, multi } = this.props
            const { files: stateFiles } = this.state
            const preparedFiles = data.map((file) => {
                file.id = id()
                file.percentage = 0

                return file
            })
            const files = multi ? [...stateFiles, ...preparedFiles] : preparedFiles

            this.setState(
                {
                    files,
                    uploaderClass: null,
                },
                () => {
                    if (autoUpload) {
                        this.startUpload(data)
                    } else {
                        onChange(data)
                        onBlur(data)
                    }
                },
            )
        }

        /**
         * @param {Array} files
         */
        onDropRejected(files) {
            const { accept } = this.props
            const { files: stateFiles } = this.state
            const errorText = `Ошибка формата файла. Ожидаемый тип: ${accept}`

            this.setState({
                files: [...stateFiles, ...files.map((file) => {
                    file.error = errorText
                    file.id = id()
                    file.percentage = 0

                    return file
                })],
            })
        }

        /**
         * Загрузка изображений в state
         * @param files
         */
        handleImagesDrop(files) {
            const { accept, t } = this.props
            let errorText = `${t('imageUploadAvailableImageTypes')} JPG/PNG/SVG`

            if (accept) {
                errorText = `${t('imageUploadAvailableImageTypes')} ${accept}`
            }

            if (everyIsValid(files) && files.length !== 0) {
                this.setState({
                    imgError: {},
                })

                const { imgFiles } = this.state

                this.setState({
                    imgFiles: imgFiles.concat(files),
                })

                this.handleDrop(files)
            } else {
                this.setState({
                    imgError: {
                        message: errorText,
                    },
                })
            }
        }

        /**
         * Удаление из стейта
         * @param index
         * @param id
         */
        handleRemove(index, id) {
            const {
                value = [],
                multi,
                valueFieldId,
                onChange,
                onBlur,
                deleteUrl,
                onDelete,
                deleteRequest,
            } = this.props

            const { files, imgFiles } = this.state

            this.setState({
                imgError: {},
            })

            const fileToBeDeleted = find(files, ({ id: idFromState }) => idFromState === id)
            const isUploading = !has(fileToBeDeleted, 'response') && !has(fileToBeDeleted, 'error')

            const fileDeletionExecutor = () => {
                if (isUploading) {
                    fileToBeDeleted.cancelSource.cancel()
                }

                if (!deleteUrl) {
                    return
                }

                if (isFunction(deleteRequest)) {
                    deleteRequest(id)

                    return
                }

                deleteFile(this.resolveUrl(deleteUrl), id)
                onDelete(index, id)
            }

            fileDeletionExecutor()

            const newFiles = files.slice()
            const newImgFiles = imgFiles.slice()

            newFiles.splice(index, 1)
            newImgFiles.splice(index, 1)
            this.setState({
                files: [...newFiles],
                imgFiles: [...newImgFiles],
            })

            if (value) {
                const newValue = multi
                    ? value.filter(f => f[valueFieldId] !== id)
                    : null

                onChange(newValue)
                onBlur(newValue)
            }
        }

        /**
         * Изменение компонента
         */
        handleChange(newFile) {
            const { value, multi, onChange } = this.props

            onChange(multi ? [...(value || []), newFile] : newFile)
        }

        /**
         * Start upload files
         * @param files
         */
        startUpload(files) {
            const {
                labelFieldId,
                sizeFieldId,
                requestParam,
                uploadUrl,
                onStart,
                uploadRequest,
            } = this.props

            const { files: preparedFilesFromState } = this.state
            const url = this.resolveUrl(uploadUrl)

            this.setState({
                uploading: reduce(
                    preparedFilesFromState,
                    (acc, file) => {
                        const { id, error } = file

                        if (!has(file, 'status')) {
                            acc = { ...acc, [id]: !error }
                        }

                        return acc
                    },
                    {},
                ),
            })

            files.forEach((file) => {
                if (file.error) {
                    // Не загружаем файлы, которые не прошли префильтры

                    return
                }
                if (!this.requests[file.id]) {
                    const onProgress = this.onProgress.bind(this, file.id)
                    const onUpload = this.onUpload.bind(this, file.id)
                    const onError = this.onError.bind(this, file.id)

                    file.cancelSource = axios.CancelToken.source()

                    if (labelFieldId !== 'name') {
                        file[labelFieldId] = file.name
                    }
                    if (sizeFieldId !== 'size') {
                        file[sizeFieldId] = file.size
                    }

                    const formData = new FormData()

                    formData.append(requestParam, file)
                    onStart(file)

                    if (isFunction(uploadRequest)) {
                        uploadRequest(formData, onProgress, onUpload, onError)
                    } else {
                        this.requests[file.id] = undefined

                        post(
                            url,
                            formData,
                            onProgress,
                            onUpload,
                            onError,
                            file.cancelSource,
                        )
                    }
                }
            })
        }

        /**
         * Change upload progress
         * @param id
         * @param event
         */
        onProgress(id, event) {
            if (event.lengthComputable) {
                this.onLoading(event.loaded / event.total, id)
            }
        }

        /**
         * Loading event
         * @param percentage
         * @param id
         */
        onLoading(percentage, id) {
            const { files } = this.state

            this.setState({
                files: [
                    ...files.map((file) => {
                        if (file.id === id) {
                            file.percentage = percentage
                        }

                        return file
                    }),
                ],
            })
        }

        /**
         * Call upload function
         */
        onStartUpload() {
            const { files } = this.state

            this.startUpload(files)
        }

        /**
         * Upload event
         * @param id
         * @param response
         */
        onUpload(id, response) {
            const { onSuccess } = this.props

            if (response.status < 200 || response.status >= 300) {
                this.onError(id, response.statusText, response.status)
            } else {
                const file = response.data
                const { files, uploading } = this.state

                this.setState({
                    files: [
                        ...files.map((item) => {
                            if (item.id === id) {
                                return {
                                    ...this.fileAdapter(file),
                                    loading: false,
                                }
                            }

                            return item
                        }),
                    ],
                    uploading: {
                        ...uploading,
                        [id]: false,
                    },
                })
                this.requests[id] = undefined
                onSuccess(response)
                this.handleChange(file)
            }
        }

        onError(id, error) {
            const { responseFieldId, onError } = this.props

            const { uploading, files } = this.state

            if (uploading) {
                uploading[id] = false
            }
            this.setState({
                uploading,
                // eslint-disable-next-line array-callback-return
                ...files.map((file) => {
                    if (file.id === id) {
                        let formattedError

                        if (onError) {
                            formattedError = onError(error)
                        } else {
                            formattedError = (
                                get(error, `response.data[${responseFieldId}]`, null) ||
                                error.message ||
                                error.status
                            )
                        }

                        file.error = formattedError
                    }
                }),
            })
        }

        onDragEnter() {
            this.setState({
                uploaderClass: 'n2o-file-uploader-event-drag-enter',
            })
        }

        onDragLeave() {
            this.setState({
                uploaderClass: null,
            })
        }

        render() {
            return 
        }
    }

    ReturnedComponent.contextTypes = {
        _reduxForm: PropTypes.string,
    }

    ReturnedComponent.defaultProps = {
        t: () => {},
        requestParam: 'file',
        visible: true,
        icon: 'fa fa-upload',
        statusBarColor: 'success',
        multi: true,
        disabled: false,
        autoUpload: true,
        showSize: true,
        value: [],
        sizeFieldId: 'size',
        onChange: () => {},
        onBlur: () => {},
        onStart: () => {},
        onSuccess: () => {},
        onDelete: () => {},
    }

    return WithPropsResolver(ReturnedComponent)
}

export default FileUploaderControl




© 2015 - 2025 Weber Informatics LLC | Privacy Policy