npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@nolesh/react-file-manager

v2.2.1

Published

Simple HTML5 file manager with drag-drop zone to access, edit, delete, upload, download and sort files

Downloads

8

Readme

react-file-manager logo

React File Manager

npm (scoped) GitHub Workflow Status Coverage Status GitHub GitHub

A simple HTML5 file manager with a drag and drop zone used to cover the most common file operations such as access, edit, delete, upload, download and sort files.

react file manager demo

Click here to access the full list of live examples hosted on CodeSandbox.


Key Features

  • No dependent packages (includes all necessary components like button, menu, etc.).
  • Fully customizable.
  • Provides preview for all major images, video and audio files.
  • Supports uploading multiple files in one request.
  • Take control of upload lifecycle.
  • Written in TypeScript and has high test coverage.
  • Ships with types included.
  • All texts and errors in the component can be localized into any supported language.

TypeScript Support

React File Manager supports TypeScript and ships with types in the library itself; no need for any other install.

Installation

To install React File Manager, use the following command:

# via npm
npm install @nolesh/react-file-manager

# via yarn
yarn add @nolesh/react-file-manager

React File Manager has react and react-dom as peer dependencies. React version is 16.8 or above is required because we use hooks.

Usage

You can use this component as a simple file uploader. The following code gives you a sortable dropzone and clickable file input that only accepts image files. It uploads files to https://httpbin.org/post and then removes the items that have been uploaded.

import React from 'react';
import FileManager from '@nolesh/react-file-manager';
// make sure you include the stylesheet otherwise the file manager will not be styled
import '@nolesh/react-file-manager/dist/styles.css';

const FileUploader = () => (
    <FileManager
        getUploadParams={(files) => ({
            URL: `https://httpbin.org/post`,
            // If the server returns anything other than null,
            // we should return it to remove uploaded file from the list.
            processResponse: () => null,
        })}
        accept='image/*'
    />
);

It takes a little more code to set it up as a file manager. In addition to the previous example, this file manager receives two image files from https://httpbin.org/image and displays them, has the ability to view, download and delete files. It also handles errors that may occur during file management.

import React from 'react';
import FileManager from '@nolesh/react-file-manager';
// make sure you include the stylesheet otherwise the file manager will not be styled
import '@nolesh/react-file-manager/dist/styles.css';

const url = 'https://httpbin.org';

const blobToBase64 = (blob) => (
  new Promise((resolve) => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result);
    reader.readAsDataURL(blob);
  })
)

const fetchFile = (name, type) => (
    fetch(`${url}/image/${type}`)
    .then(res => res.blob())
    .then(async blob => {
        const base64 = await blobToBase64(blob);
        return {
            id: type,
            fileName: `${name}.${type}`,
            fileSize: blob.size,
            previewData: {
                src: base64
            }
        }
    })
)

const viewOrDownloadFile = (fileData) => (
    !!fileData.id
    ? fetch(`${url}/image/${fileData.id}`).then((resp) => resp.blob())
    : Promise.reject(`File (${fileData.fileName}) not found`)
)


const MySimpleFileManager = () => (
    <FileManager
        fetchRemoteFiles={
            () => Promise.all([
                fetchFile('piggy', 'png'),
                fetchFile('wolf','webp')
            ])
        }
        getUploadParams={(files) => ({
            URL: `${url}/post`,
            fields: {
                name: files[0].fileName,
                size: files[0].fileSize,
            },
            processResponse: (res) => {
                // If the server returns anything other than uploaded file data, we should return:
                // Correct file data from response
                return {
                    fileName: res.form.name,
                    fileSize: res.form.size,
                    previewData: {
                        src: res.files.file
                    }
                }
                // Or files passed as function argument to use the internal file data.
                // return files;
            },
            // Retrieves the error message from the server error response
            // Since our error response is an object containing a status and a message,
            // we need to convert it to a string
            processError: error => error.message
        })}
        viewFile={viewOrDownloadFile}
        downloadFile={viewOrDownloadFile}
        // The server doesn't support file deletion, so we simulate it
        deleteFile={(fileData) => (
            new Promise((resolve, reject) => {
                if (window.confirm('Are you sure you want to delete this file?')) {
                    // In real app we should pass the filename or id
                    fetch(`${url}/delete`, {
                        method: 'DELETE'
                    })
                    .then((response) => response.json())
                    .then(() => resolve())
                    .catch((err) => reject(err.message));
                }
                else reject();
            })
        )}
        // We can get several errors, so we combine them and show to the user
        onError={(err) => {
            if (!Array.isArray(err)) alert(err.message);
            else {
                const messages = err.reduce(
                    (acc, val, indx) => acc.concat(`${indx + 1}. ${val.message}\n`),
                    ''
                );
                alert(messages);
            }
        }}
        accept='image/png,image/jpeg'
    />
);

See more live examples here.

Props & Methods

|Name|Type|Default Value|Description| |---|---|---|---| |accept|string|'*'|Set accepted file types. Keep in mind that mime type determination is not reliable across platforms. CSV files, for example, are reported as text/plain under macOS but as application/vnd.ms-excel under Windows. This prop must be a valid MIME type according to input element specification or a valid file extension.| |multiple|bool|true|Allow drag 'n' drop (or selection from the file dialog) of multiple files.| |maxFileCount|number|undefined|Maximum number of files allowed for local (accepted) and remote (uploaded) files. The default value is undefined, which indicates no limitation on the number of accepted files.| |maxFileSize|number|undefined|The maximum file size in bytes| |minFileSize|number|0|The minimum file size in bytes| |disabled|bool|false|Enable or disable the file manager.| |readOnly|bool|false|Set the file manager to read-only mode.| |noClick|bool|false|If true, disables click to open the native file selection dialog.| |noDrag|bool|false|If true, disables the drag 'n' drop feature.| |noKeyboard|bool|false|If true, disables SPACE/ENTER to open the native file selection dialog.| |autoUpload|bool|false|If true, automatically uploads files as soon as they are accepted.| |checkFileDuplicates|'none'\|'local'\|'remote'\|'all'|'all'|Checks local (accepted) and/or remote (uploaded) files for duplicates on acceptance and skips them (throw an error with file_exists code) if they exist and the appropriate mode is selected.One of type: 'none', 'local', 'remote', 'all'.| |preventDropOnDocument|bool|true|If false, allow dropped items to take over the current browser window.| |addFileDescription|bool|false|Enables the user to add a custom description to a file.| |uploadFilesInOneRequest|bool|false|If true, multiple files will be uploaded in one request.| |tabIndex|number|0|Set the tabindex attribute on the root component.| |overrides|TOverrides|null|Allows overriding the root and file item components. See overrides for more information.| |getRoot|(root: HTMLElement) => void|null|Ref callback that gets a DOM reference to the root body element. Can be useful to programmatically scroll.| |getUploadParams|TGetUploadParams|null|Set parameters such as URL, headers, etc. needed for upload files. See getUploadParams for more information.| |fetchRemoteFiles|() => Promise<IFileData[]>|() => Promise.resolve([])|Fetch remote files from the server. See fetchRemoteFiles for more information.| |fileFieldMapping|(data: any) => IFileData|null|Map the file fields received from the server to the file manager's file fields. This property can be omitted if the file manager's file fields match the fields on the server. See fileFieldMapping for more information.| |viewFile|(fileData: IRemoteFileData) => Promise<Blob \| void>|null|View remote file. See viewFile for more information.| |downloadFile|(fileData: IRemoteFileData) => Promise<{ blob: Blob; fileName: string } \| Blob \| void>|null|Download remote file. See downloadFile for more information.| |deleteFile|(fileData: IRemoteFileData) => Promise<void>|null|Remove remote file from the server. See deleteFile for more information.| |setFileDescription|(fileData: IRemoteFileData) => Promise<string>|null|Set the description for the remote file. See setFileDescription for more information.| |sortFiles|TSortFunc|null|Sort local and remote files. See sortFiles for more information.| |filePreview|TFilePreview|() => Promise.resolve()|Allow to add previews for unsupported file types or override the default implementation. See filePreview for more information.| |fileValidator|TFileValidator|null|A custom function for file validation. See fileValidator for more information.| |onFilesUploaded|(fileData: IRemoteFileData[]) => void|null|Callback for when files have been uploaded.| |onUploadProgress|(progress: number, sentBytes: number, totalBytes: number) => void|null|Callback for when files are uploading. Fires every 100ms.| |onChangeLocalFileStack|(result: ILocalFileData[], changedFiles: ILocalFileData[]) => void|null|Callback for when any of the local files have been added or removed.| |onChangeItemMountStates|(changedItems: IItemMountState[], mountedItems: IItemMountState[], unmountedItems: IItemMountState[]) => void|null|Callback for when any of the file items (local and/or remote) have been mounted or unmounted.| |onLoading|(isLoading: boolean, isUploading: boolean) => void|null|Callback for when files are fetching or uploading.| |onDropFiles|(acceptedFiles: File[], fileRejections: { file: File; errors: TInternalError[] }[]) => void|null|Callback for when files are accepted or rejected based on the accept, multiple, minFileSize and other props.| |onError|TOnError|null|Callback for when an error occurs. See onError for more information.| |onUnmountComponent|(root: HTMLDivElement, fileInput: HTMLInputElement) => void|null|Callback for when component is unmounted.|

overrides

This property allows the user to override the appearance and text of all components within the file manager, or replace them with custom components. It is also possible to change the implementation of the uidGenerator and fileSizeFormatter functions, which generate a unique identifier for files and make a human-readable format for the file size, respectively. The overrides property is an object with the following structure:

{
	uidGenerator?: () => string,
	fileSizeFormatter?: /* TFileSizeFormatter */ (size: number) => string,
	Root?: /* IOverriddenRoot */ {
		hideHeader?: boolean,
		hideFooter?: boolean,
		texts?: {
			headerFileType?: string,
			headerFileName?: string,
			headerFileSize?: string,
			footer?: string,
			dragActiveAccept?: string,
			dragActiveReject?: string,
			loading?: string,
			defaultText?: string,
			defaultTextDisabled?: string
		},
		classNames?: {
		    dropZone?: string,
		    activeAccept?: string,
		    activeReject?: string,
		    header?: string,
		    footer?: string
		},
		styles?: {
			dropZone?: React.CSSProperties,
			header?: React.CSSProperties,
			footer?: React.CSSProperties
		},
		// Replaces default root to custom component
		component?: (props: IRootComponentArgs) => React.ReactElement
	},
	FileItem?: /* IOverriddenFileItem */ {
		titles?: {
			menuButtonTitle?: string,
			menuItemView?: string,
			menuItemDownload?: string,
			menuItemRename?: string,
			menuItemDelete?: string,
		},
		rootStyles?: () => {
		    base?: {
                className?: string,
                style?: React.CSSProperties
            },
            local?: {
                className?: string,
                style?: React.CSSProperties
            },
            localDisabled?: {
                className?: string,
                style?: React.CSSProperties
            },
            uploading?: {
                className?: string,
                style?: React.CSSProperties
            },
            uploaded?: {
                className?: string,
                style?: React.CSSProperties
            },
            uploadedDisabled?: {
                className?: string,
                style?: React.CSSProperties
            },
            uploadError?: {
                className?: string,
                style?: React.CSSProperties
            },
            deletionError?: {
                className?: string,
                style?: React.CSSProperties
            },
            editMode?: {
                className?: string,
                style?: React.CSSProperties
            }
		},
		thumbnailFieldStyles?: /* TThumbnailFieldStyles */ ({
            fileData: ILocalFileData,
            readOnly: boolean,
            disabled: boolean
        }) => ({
            container?: React.CSSProperties,
            type?: React.CSSProperties,
            loading?: React.CSSProperties,
            audio?: {
                type: React.CSSProperties,
                duration: React.CSSProperties,
                buttonContainer: React.CSSProperties,
                buttonStart: React.CSSProperties,
                buttonStop: React.CSSProperties,
            },
            image?: React.CSSProperties,
            duration?: React.CSSProperties,
            default?: React.CSSProperties
        }),
		thumbnailFieldComponent?: /* TThumbnailFieldComponent */ ({
            fileData: ILocalFileData,
            readOnly: boolean,
            disabled: boolean
        }) => React.ReactElement,
		inputFieldStyles?: /* TInputFieldStyles */ ({
            fileData: ILocalFileData,
            readOnly: boolean,
            disabled: boolean
        }) => ({
		    readOnlyText?: {
                className?: string,
                style?: React.CSSProperties
            },
            textField?: {
                className?: string,
                style?: React.CSSProperties
            }
		},
		inputFieldComponent?: /* TInputFieldComponent */ ({
		    fileData: ILocalFileData;
            readOnly: boolean;
            disabled: boolean;
            changeDescription: (e: React.ChangeEvent<HTMLInputElement>) => void;
            changeDescriptionMode: () => void;
            confirmDescriptionChanges: () => Promise<void>;
            undoDescriptionChanges: () => void;
            getInputFieldProps: () => /* IInputFieldProps */ {
                id: string;
                value: string;
                title: string;
                onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
                onKeyUp: (e: React.KeyboardEvent) => void;
                onFocus: (e: React.FocusEvent) => void;
                onBlur: (e: React.FocusEvent) => void;
            }
		}) => React.ReactElement,
		sizeFieldStyle?: /* TSizeFieldStyle */ ({
            fileData: ILocalFileData,
            readOnly: boolean,
            disabled: boolean
        }) => {
            className?: string,
            style?: React.CSSProperties
		},
		sizeFieldComponent?: /* TSizeFieldComponent */ ({
            fileData: ILocalFileData,
            readOnly: boolean,
            disabled: boolean,
            formatSize: /* TFileSizeFormatter */ (size: number) => string
        }) => React.ReactElement,
        controlField?: /* TControlField */ {
            buttons?: /* TControlFieldButtons */ ({
                fileData: ILocalFileData,
                readOnly: boolean,
                disabled: boolean
            }) => {
                uploadFile?: {
                    props?: {
                        style?: React.CSSProperties,
                        className?: string,
                        title?: string
                    },
                    children?: React.ReactElement
                },
                removeLocalFile?: {
                    props?: {
                        style?: React.CSSProperties,
                        className?: string,
                        title?: string
                    },
                    children?: React.ReactElement
                },
                confirmDescription?: {
                    props?: {
                        style?: React.CSSProperties,
                        className?: string,
                        title?: string
                    },
                    children?: React.ReactElement
                },
                undoDescription?: {
                    props?: {
                        style?: React.CSSProperties,
                        className?: string,
                        title?: string
                    },
                    children?: React.ReactElement
                },
                loadingIcon?: React.ReactElement
            },
            menu?: /* TControlFieldMenu */ ({
                fileData: ILocalFileData,
                readOnly: boolean,
                disabled: boolean
            }) => {
                buttonProps?: {
                    title?: string,
                    className?: string,
                    style?: React.CSSProperties
                },
                buttonChildren?: React.ReactElement,
                displayIcons?: boolean,
                menuStyles?: {
                    layer?: React.CSSProperties,
                    menu?: React.CSSProperties
                },
                menuItemStyle?: React.CSSProperties,
                menuItemNames?: {
                    menuItemView?: string,
                    menuItemDownload?: string,
                    menuItemRename?: string,
                    menuItemDelete?: string
                }
            },
            component?: /* TControlFieldComponent */ ({
                fileData: ILocalFileData,
                readOnly: boolean,
                disabled: boolean
                noKeyboard: boolean,
                changeDescription: (e: React.ChangeEvent<HTMLInputElement>) => void,
                changeDescriptionMode: () => void,
                confirmDescriptionChanges: () => Promise<void>,
                deleteFile: (fileData: ILocalFileData | IRemoteFileData) => void,
                downloadFile: (fileData: IRemoteFileData) => void,
                undoDescriptionChanges: () => void,
                viewFile: (fileData: IRemoteFileData) => void,
                uploadFile?: (fileData: ILocalFileData) => void,
            }) => React.ReactElement
        },
		progressBarComponent?: /* TProgressBarComponent */ (progress: number) => React.ReactElement,
		readOnlyIconComponent?: /* TReadOnlyIconComponent */ () => React.ReactElement,
		// Replaces default file item to custom component
		component?: (props: IFileItemComponentArgs) => ReactElement
	}
}

All object keys are independent and optional. Using this property, the user can redefine almost any part of the File Manager. For more information, see examples (Custom UI, Overridden UI).

getUploadParams

This function should return an object or promise with the parameters required to upload files:

type TUploadParams = {
    URL: string;
    fileFieldName?: string;
    fields?: { [name: string]: string | Blob };
    body?: BodyInit;
    headers?: { [name: string]: string };
    method?: TMethod;
    timeout?: number;
    checkResult?: (result: any) => boolean;
    processResponse?: (response: any) => any;
    processError?: (error: any) => string;
};

getUploadParams: /* TGetUploadParams */ (
    localFileData: ILocalFileData | ILocalFileData[]
) => TUploadParams | Promise<TUploadParams>;

This property is used by the ref-accessible upload function if it is called without parameter (see list of props & methods available by ref). There are some aspects to consider when using getUploadParams:

  • The localFileData argument can be either a single file data or an array of file data. It depends on the uploadFilesInOneRequest property, which is set to false by default, meaning that the localFileData is a single file data. On the other hand, if you use mode of uploading files in one request (uploadFilesInOneRequest=true), the localFileData will be an array of file data.
  • When the addFileDescription property is set to true, the user has the ability to attach a description to the uploaded file. Depending on the server implementation, you can use headers or fields to send this description and other data along with the file.
  • If the uploadFilesInOneRequest property is set to true, all local (accepted) files will be wrapped in FormData and sent in a single request. Accordingly, all necessary data, such as file descriptions, must be attached to this request and further processed on the server. You can pass file data as an array using fields (adds an additional field to FormData) or headers, as shown below:
    const getUploadParams: TGetUploadParams = (localFileData) => ({
        headers: {
            attachments: JSON.stringify(
                Array.isArray(localFileData)
                    ?
                    localFileData.map((fileData) => ({
                        fileId: fileData.uid,
                        description: encodeURIComponent(fileData.description)
                    }))
                    :
                    {
                        fileId: localFileData.uid,
                        description: encodeURIComponent(localFileData.description)
                    }
            ),
        },
        // OR
        fields: {
            fileIds: JSON.stringify(
                Array.isArray(localFileData)
                    ? localFileData.map(f => f.uid)
                    : localFileData.uid
            ),
            descriptions: JSON.stringify(
                Array.isArray(localFileData)
                    ? localFileData.map(f => f.description)
                    : localFileData.description
            )
        },
        ...
    });
  • processResponse is an intermediate function for processing the result received from the server after uploading file(s). If the server returns null or data that can be correctly mapped by the fileFieldMapping property, processResponse can be omitted. Otherwise it should return one of the following:
    • proper file data created from the server response, which must be correctly mapped by the fileFieldMapping or contain required fields (fileName, fileSize).
    • localFileData passed as an argument to the getUploadParams function in order to use the internal file data.
    • null to remove uploaded file(s) from the list.
  • checkResult is an optional function that checks the result received from the server to make sure the file was loaded correctly. If it is defined and returns false, the file manager will throw an error even if the file was successfully uploaded.
  • processError is a function to process an error that may be thrown by the server during a file upload. It should extract and return the text from the error for the message that will be displayed to the user in the onError property.
  • In some situations, you may need to use the body property instead of FormData. For example, uploading a file to S3 using PUT. Basically, if you use PUT, you can't wrap your file in a FormData instance. In this case, body must be set to file:
    body: localFileData.file,
    method: 'PUT',
  • It is also possible to pass a promise to the getUploadParams function if you want to perform preliminary actions, such as asynchronously obtaining an access token:
    const getUploadParams: TGetUploadParams = async (localFileData) => {                            
        return {
            URL: '/api/uploadFile',
            headers: {
                Authorization: await getAccessToken()
            }
        }
    }

fetchRemoteFiles

This property is a function for getting uploaded (remote) files from the server when the File Manager component is mounted. By default it returns Promise.resolve([]). This property is also used by the ref-accessible fetchRemoteFiles function if it is called without parameter (see list of props & methods available by ref). Basic example:

const fetchRemoteFiles = () => (
    fetch(`/api/fetchFiles`)
    .then(res => res.json())
    .catch(e => Promise.resolve([]))
)

fileFieldMapping

This function maps the server response to the fields of the component file item. If this property is NOT defined and the server response does NOT contain required fields such as fileName and fileSize, the component will throw a specific error. It can be omitted if the field mapping is implemented in the fetchRemoteFiles and processResponse. In a real application, the server should send a response with the same structure when fetching and after uploading files. So you can use this property to map the server response in both cases instead of doing this individually in the fetchRemoteFiles and processResponse. It can also be used to extend file data with additional fields.

const fileFieldMapping = (data: any) => ({
    uid: data.file_id, // optional
    fileName: data.file_name, // required!
    fileSize: data.file_size, // required!
    fileType: data.file_type, // optional
    description: data.desc, // optional
    previewData: { // optional
        src: data.thumbnail, // optional
    },
    readOnly: data.read_only, // optional
    disabled: !data.enabled, // optional
    myCustomField: data.my_custom_field, // you can add extra fields to the file data
})

IMPORTANT:

  • fileName and fileSize fields are mandatory.
  • In addition to the above fields, the File Manager can add extra fields, such as, for example, elementRef, needed to navigate to a file element, and others for internal use. So make sure the following keys are NOT overlapped:
    • file
    • state
    • oldDescription
    • editMode
    • elementRef
    • cancelUpload
    • uploadedSize
    • totalSize
    • shouldBeRemoved

viewFile

This property should provide file browsing implementation. It must return a promise that resolves with a blob, or an empty promise in the case of a custom implementation. Example of requesting a blob from the server and handling errors if they exist:

const viewFile: IFileManagerProps['viewFile'] = (fileData) => (
    fetch(`/api/file/${fileData.fileName}`)
    .then((resp) => resp.blob())
)

Custom implementation example:

const viewFile: IFileManagerProps['viewFile'] = (fileData) => {
    if(!fileData?.previewData?.src) return Promise.reject('Something went wrong!');

    var image = new Image();
    image.src = fileData.previewData.src;
    var w = window.open("");
    w.document.write(image.outerHTML);

    return Promise.resolve();
}

downloadFile

This property should provide file downloading implementation. It must return a promise that resolves with a blob or an object that contains blob and fileName, or an empty promise in the case of a custom implementation. Example of requesting a blob from the server and handling errors if they exist:

const downloadFile: IFileManagerProps['downloadFile'] = (fileData) => (
    fetch(`/api/file/${fileData.fileName}`)
    .then((resp) => resp.blob())
)

Custom implementation example:

const downloadFile: IFileManagerProps['downloadFile'] = async (fileData) => {
    if(!fileData?.previewData?.src) return Promise.reject('Something went wrong!');

    // convert base64 to blob
    const blob = await fetch(fileData.previewData.src).then(resp => resp.blob());
    const fileName = 'custom_'+fileData.fileName;

    // save the file under a new name
    return Promise.resolve({blob, fileName});
}

deleteFile

This property should provide an implementation for deleting files. It is required to return a resolved promise to confirm file deletion. To cancel the deletion of a file, the user need to return an empty promise rejection:

const deleteFile: IFileManagerProps['deleteFile'] = (fileData) => {
    return new Promise((resolve, reject) => {
        // to give the menu time to disappear, we put the 'window.confirm' inside 'setTimeout'
        setTimeout(() => {
            if (window.confirm('Are you sure you want to delete this file?')) {
                fetch(`/api/file/${fileData.uid}`, {
                    method: 'DELETE'
                })
                .then((response) => response.json())
                .then((resp) => {
                    if (resp.result) resolve(); // confirm file deletion
                    else reject(resp.error); // cancel file deletion in case of an error
                })
                .catch((err) => reject(err)); // cancel file deletion in case of an error
            }
            else reject(); // cancel the deletion of the file if the user clicks the "Cancel" button
        }, 10);
    });
}

setFileDescription

This property should provide an implementation for setting the file description. Do not confuse this property with the addFileDescription. The setFileDescription sends a request to the server to set a description for an already uploaded file, whereas addFileDescription makes the ability to attach a description to a file at upload time. Basic example:

const setFileDescription: IFileManagerProps['setFileDescription'] = (fileData) => (
    fetch(`/api/file/${fileData.uid}`, {
        method: 'PATCH',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({description: fileData.description})
    })
    .then((response) => (
        response.ok ? response.text() : Promise.reject(response.statusText)
    ))
)

Note that this function returns some text from the server. This text will be set as the file description in the component.

sortFiles

This property should provide an implementation for sorting files. The following example sorts files by their types (local files come first) and size:

const sortFiles: TSortFunc = (a, b) => {
    // sort by file type (local or remote)
    const fileTypeResult = a.isLocalFile === b.isLocalFile ? 0 : a.isLocalFile ? -1 : 1;
    // sort by file size
    const fileSizeResult = a.fileData.fileSize - b.fileData.fileSize;
    return fileTypeResult || fileSizeResult;
}

Note that this property overrides the default sort function implemented in the root component.

filePreview

This property allows the user to add previews for unsupported file types or override the default implementation. The default value is () => Promise.resolve() which means use the default implementation. Note that filePreview is for local files only. Preview creation for uploaded (remote) files should be implemented on the server side. The following example adds an implementation for files of type PDF:

// We are using the 'PDFJS' library to generate the thumbnail of the PDF file.
// Feel free to use any other library for this purpose.
const PDFJS = require('pdfjs-dist/webpack');
...
const filePreview: IFileManagerProps['filePreview'] = (file) => {
    if (file.type === 'application/pdf') {
        const promise = new Promise<string>((resolve) => {
            const objectUrl = URL.createObjectURL(file);
            PDFJS.getDocument(objectUrl).promise.then((pdf: any) => {
                pdf.getPage(1).then((page: any) => {
                    const scale = '1.5';
                    const viewport = page.getViewport({
                        scale: scale,
                    });
                    const canvas = document.createElement('canvas');
                    const canvasContext = canvas.getContext('2d');
                    canvas.height =
                        viewport.height || viewport.viewBox[3];
                    canvas.width =
                        viewport.width || viewport.viewBox[2];
                    page.render({
                        canvasContext,
                        viewport,
                    }).promise.then(() => resolve(canvas.toDataURL()));
                });
            });
        });

        // Prevents a long loading process (optional)
        const result = Promise.race([
            promise,
            new Promise<void>((resolve) => {
                setTimeout(resolve, 1000);
            }),
        ]).then((result) => {
            if (!result) console.warn('It takes too long to create a PDF thumbnail');
            return result;
        });

        return result;
    }

    // Return a promise that resolves with a 'null' or 'undefined' 
    // to use the default implementation for the rest file types.
    return Promise.resolve();
}

You can also override the default implementation for image, audio, and video file types:

const filePreview: IFileManagerProps['filePreview'] = (file) => {
    // Overrides default implementation for images
    if(file.type.startsWith('image/')){
      const imageUrl = URL.createObjectURL(file);
      return Promise.resolve(imageUrl);
    }
    // Prevent the default implementation if the file type is audio (ignore audio files)
    else if (file.type.startsWith('audio/')) {
        return Promise.reject();
    }

    // Prevent the default implementation for the rest file types (video)
    return Promise.reject();
}

fileValidator

Custom file validation function. It should return a single error, or an array of errors if the check fails. If validation succeeds, return null. The error should be an object with the following structure:

{
    errorId: 'user-error-id',
    message: `human readable error message`,
    data: {} // additional data attached to the error
}

The following example checks the length of a file name:

const fileValidator: TFileValidator = (file, localFiles, remoteFiles) => {
    const maxLength = 20; // file name is larger than 20 symbols
    if(file.name.length > maxLength) {
        return {
            errorId: 'name-too-large',
            message: `Name is larger than ${maxLength} characters`, // required
            data: { file, maxLength }
        };
    }
    return null;
}

Note that if you do NOT set the errorId key, it will be set to the default value validation_error.

onError

Callback for when an error occurs. It may receive a single error or an array of errors as an argument. In case of multiple errors, you can collect all errors and show them to the user in one message:

const onError: TOnError = (err) => {
    if (Array.isArray(err)) {
        const messages = err.reduce(
            (acc, val, indx) => acc.concat(`${indx + 1}. ${val.message}\n`),
            ''
        );
        alert(messages);
    }
}

Since the error has fields such as errorId and data, you can ignore or override certain types of errors or extend the message with extra information:

const onError: TOnError = (err) => {
    const processError = e => {
        if(e.errorId === 'invalid_size_min') { // override default message
            return `file size is too small (${e.data.fileSize})`
        }
        else if(e.errorId === 'invalid_size_max') return null; // ignore error
        else return e.message; // return default message
    }

    let message = [];

    if (Array.isArray(err)) {
        message = err.map(e => processError(e)).filter(e => e);
    }
    else message.push(processError(err));

    if(message.length) alert(message.join('\n'));
}

Full list of internal error codes: |Code|Description| |---|---| |upload_aborted|Occurs when the user aborts a file uploading.| |upload_timeout|Occurs when a file uploading is interrupted due to a timeout.| |upload_wrong_result|Occurs when the response from the server fails the check in the "checkResult" function.| |upload_error|Occurs when a file uploading is interrupted due to a lost connection or a server-side error.| |delete_error|Occurs when the deleteFile function returns a promise rejection with an error, represented as a string.| |download_error|Occurs when the downloadFile function returns a promise rejection.| |view_error|Occurs when the viewFile function returns a promise rejection.| |rename_error|Occurs when the setFileDescription function returns a promise rejection.| |multiple_not_allowed|Occurs when the user tries to add multiple files at once while the multiple property is set to false.| |exceed_max_file_count|Occurs when the user tries to add more files than specified in the maxFileCount property.| |invalid_size_max|Occurs when the user tries to add a file that is larger than the value specified in the maxFileSize property.| |invalid_size_min|Occurs when the user tries to add a file that is smaller than the value specified in the minFileSize property.| |invalid_type|Occurs when the user tries to add a file whose type is NOT specified in the accept property.| |file_exists|Occurs when the user tries to add a file that already exist. The occurrence of this error depends on the checkFileDuplicates property. For example, if the user adds a file that is already uploaded and checkFileDuplicates is set to remote, an error occurs. If you set checkFileDuplicates to none, this error will be ignored.| |validation_error|This error is related to the fileValidator function. This function should return an error (or an array of errors), if file validation fails. The default code of this error is validation_error.| |unexpected_error|This error code is currently unused and left for possible future use.|

Ref

You can use the ref exposed by the ReactFileManager component to perform some actions programmatically, such as opening a file dialog.

import React, {useRef} from 'react';
import FileManager, { IFileManagerRef } from '@nolesh/react-file-manager';
// make sure you include the stylesheet otherwise the file manager will not be styled
import '@nolesh/react-file-manager/dist/styles.css';

const uploadParams: TGetUploadParams = (localFileData) => ({
    URL: `https://httpbin.org/post`,
    // If the server returns anything other than null,
    // we should return it to remove uploaded file from the list.
    processResponse: () => null,
});

const FileUploader = () => {

    const fileManagerRef = useRef<IFileManagerRef>(null);

    const openDialog = () => {
      // Note that the ref is set async, so it might be null at some point
      if (fileManagerRef.current) {
        fileManagerRef.current.openFileDialog();
      }
    };

    return (
        <div style={{ textAlign:'center' }}>
            <FileManager
                ref={fileManagerRef}
                getUploadParams={uploadParams}
                accept='image/*'
            />
            <br/>
            <button onClick={openDialog}>Add File(s)</button>
        </div>
    )
}

List of props and methods available by ref

|Name|Type|Description| |---|---|---| |openFileDialog|() => void|Allow the user to open a file dialog programmatically.| |addLocalFiles|(files: FileList \| File \| File[]) => void|Add local files to the file manager. Can be useful when adding files from the clipboard.| |removeAllLocalFiles|() => void|Remove all local files from the file manager.| |update|() => void|Perform a forced update on the file manager.| |upload|(getUploadParams?: TGetUploadParams) => Promise<any>|Initiate uploading of all local files. If it is called without parameter, it uses the function declared in the getUploadParams property.| |cancelUpload|() => void|Cancel uploading of all files.| |fetchRemoteFiles|(request?: () => Promise<any>) => Promise<IRemoteFileData[]>|Initiate fetching of remote files. If it is called without parameter, it uses the function declared in the fetchRemoteFiles property.| |remoteFiles|IRemoteFileData[]|A list of remote files displayed in the component.| |localFiles|ILocalFileData[]|A list of local (accepted) files displayed in the component.|

Contribute

If you like to improve React File Manager fork the repo and get started by running the following:

$ git clone https://github.com/nolesh/react-file-manager.git react-file-manager
$ cd react-file-manager
$ npm install
$ npm run dev

To run the demo use the following command:

$ npm run demo

Check http://0.0.0.0:3000/ in your browser and have fun!

License

MIT licensed.