Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(file-upload) #3832

Open
wants to merge 12 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/docs/config/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,13 @@
"keywords": "dropdown, menu, selection, option list",
"path": "/docs/components/dropdown.mdx"
},
{
"key": "file-upload",
"title": "File Upload",
"keywords": "file, upload, browse",
"path": "/docs/components/file-upload.mdx",
"newPost": true
},
{
"key": "drawer",
"title": "Drawer",
Expand Down
25 changes: 25 additions & 0 deletions apps/docs/content/components/file-upload/buttonsContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const App = `
import {FileUpload} from "@nextui-org/react";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing Button import.

The code uses the Button component but doesn't import it from @nextui-org/react.

-import {FileUpload} from "@nextui-org/react";
+import {FileUpload, Button} from "@nextui-org/react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {FileUpload} from "@nextui-org/react";
import {FileUpload, Button} from "@nextui-org/react";


export default function App() {
return (
<FileUpload
multiple
buttons={(onBrowse, onAdd, onReset) => {
return <div>
<Button onClick={() => onBrowse()}>Browse Files</Button>
<Button onClick={() => onAdd()}>Add New File</Button>
<Button onClick={() => onReset()}>Remove All</Button>
</div>
}}
/>
);
}`;
Comment on lines +1 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using TypeScript for better type safety.

The component is defined as a template string without proper TypeScript types for the callback functions. This could lead to type-related issues and makes the code harder to maintain.

Consider this implementation:

-const App = `
-import {FileUpload} from "@nextui-org/react";
+const App = `
+import {FileUpload, Button} from "@nextui-org/react";
+
+interface FileUploadHandlers {
+  onBrowse: () => void;
+  onAdd: () => void;
+  onReset: () => void;
+}

 export default function App() {
   return (
     <FileUpload
       multiple
-      buttons={(onBrowse, onAdd, onReset) => {
+      buttons={({onBrowse, onAdd, onReset}: FileUploadHandlers) => {
         return <div>
           <Button onClick={() => onBrowse()}>Browse Files</Button>
           <Button onClick={() => onAdd()}>Add New File</Button>
           <Button onClick={() => onReset()}>Remove All</Button>
         </div>
       }}
     />
   );
 }`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const App = `
import {FileUpload} from "@nextui-org/react";
export default function App() {
return (
<FileUpload
multiple
buttons={(onBrowse, onAdd, onReset) => {
return <div>
<Button onClick={() => onBrowse()}>Browse Files</Button>
<Button onClick={() => onAdd()}>Add New File</Button>
<Button onClick={() => onReset()}>Remove All</Button>
</div>
}}
/>
);
}`;
const App = `
import {FileUpload, Button} from "@nextui-org/react";
interface FileUploadHandlers {
onBrowse: () => void;
onAdd: () => void;
onReset: () => void;
}
export default function App() {
return (
<FileUpload
multiple
buttons={({onBrowse, onAdd, onReset}: FileUploadHandlers) => {
return <div>
<Button onClick={() => onBrowse()}>Browse Files</Button>
<Button onClick={() => onAdd()}>Add New File</Button>
<Button onClick={() => onReset()}>Remove All</Button>
</div>
}}
/>
);
}`;


const react = {
"/App.jsx": App,
};

export default {
...react,
};
32 changes: 32 additions & 0 deletions apps/docs/content/components/file-upload/controlled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const App = `
import React from "react";
import {FileUpload} from "@nextui-org/react";
import {filesFromApi} from "./filesFromApi";

const filesFromApi= new Promise((res) => {
res([
new File(['dummy content'], "file1.jpg", {type: "image/jpeg"}),
new File(['dummy content'], "file2.jpg", {type: "image/jpeg"}),
new File(['dummy content'], "file3.jpg", {type: "image/jpeg"}),
]);
});

export default function App() {
const [files, setFiles] = React.useState([]);

React.useEffect(() => {
filesFromApi.then(files => setFiles(files));
}, []);

return (
<FileUpload files={files} />
);
}`;

const react = {
"/App.jsx": App,
};

export default {
...react,
};
58 changes: 58 additions & 0 deletions apps/docs/content/components/file-upload/customButtons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const DeleteIcon = `export const DeleteIcon = ({
fill = 'currentColor',
filled,
size,
height,
width,
label,
...props
}) => {
return (
<svg aria-hidden="true" fill="none" focusable="false" height="1em" role="presentation"
viewBox="0 0 20 20" width="1em">
<path
d="M17.5 4.98332C14.725 4.70832 11.9333 4.56665 9.15 4.56665C7.5 4.56665 5.85 4.64998 4.2 4.81665L2.5 4.98332"
stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"></path>
<path
d="M7.08331 4.14169L7.26665 3.05002C7.39998 2.25835 7.49998 1.66669 8.90831 1.66669H11.0916C12.5 1.66669 12.6083 2.29169 12.7333 3.05835L12.9166 4.14169"
stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"></path>
<path
d="M15.7084 7.61664L15.1667 16.0083C15.075 17.3166 15 18.3333 12.675 18.3333H7.32502C5.00002 18.3333 4.92502 17.3166 4.83335 16.0083L4.29169 7.61664"
stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"></path>
<path d="M8.60834 13.75H11.3833" stroke="currentColor" strokeLinecap="round"
strokeLinejoin="round" strokeWidth="1.5"></path>
<path d="M7.91669 10.4167H12.0834" stroke="currentColor" strokeLinecap="round"
strokeLinejoin="round" strokeWidth="1.5"></path>
</svg>
);
};`;

const App = `
import React from "react";
import {FileUpload} from "@nextui-org/react";
import {Button} from "@nextui-org/react";
import {DeleteIcon} from './DeleteIcon';
export default function App() {
const [files, setFiles] = React.useState([]);
return (
<FileUpload
multiple
onChange={newFiles => setFiles(newFiles)}
browseButton={<Button>Search</Button>}
uploadButton={<Button onClick={() => console.log("Files to upload:\\n" + files.map(file => file.name).join("\\n"))}>Upload Files</Button>}
addButton={<Button>+</Button>}
resetButton={<Button><DeleteIcon /></Button>}
/>
);
}`;
Comment on lines +30 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance example with proper validation and error handling

The current example could be improved to demonstrate best practices:

  1. Remove console.log and show proper upload handling
  2. Add file type and size restrictions
  3. Include error handling
 const App = `
 import React from "react";
-import {FileUpload} from "@nextui-org/react";
-import {Button} from "@nextui-org/react";
+import {FileUpload, Button} from "@nextui-org/react";
 import {DeleteIcon} from './DeleteIcon';

 export default function App() {
   const [files, setFiles] = React.useState([]);
+  const [error, setError] = React.useState("");
+
+  const handleUpload = async () => {
+    try {
+      // Add your upload logic here
+      const formData = new FormData();
+      files.forEach(file => formData.append('files', file));
+      // await uploadFiles(formData);
+    } catch (err) {
+      setError("Upload failed");
+    }
+  };

   return (
     <FileUpload
       multiple
+      accept=".jpg,.png,.pdf"
+      maxSize={5242880} // 5MB
       onChange={newFiles => setFiles(newFiles)}
       browseButton={<Button>Search</Button>}
-      uploadButton={<Button onClick={() => console.log("Files to upload:\\n" + files.map(file => file.name).join("\\n"))}>Upload Files</Button>}
+      uploadButton={<Button onClick={handleUpload}>Upload Files</Button>}
       addButton={<Button>+</Button>}
       resetButton={<Button><DeleteIcon /></Button>}
     />
   );
 }`;

Committable suggestion skipped: line range outside the PR's diff.


const react = {
"/App.jsx": App,
"/DeleteIcon.jsx": DeleteIcon,
};

export default {
...react,
};
18 changes: 18 additions & 0 deletions apps/docs/content/components/file-upload/file-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const App = `
import {FileUpload} from "@nextui-org/react";

export default function App() {
return (
<FileUpload
fileItemElement={(file) => <div className="text-red-500">{file.name}</div>}
/>
);
}`;

const react = {
"/App.jsx": App,
};

export default {
...react,
};
17 changes: 17 additions & 0 deletions apps/docs/content/components/file-upload/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import usage from "./usage";
import multiple from "./multiple";
import topbar from "./topbar";
import fileItem from "./file-item";
import customButtons from "./customButtons";
import buttonsContainer from "./buttonsContainer";
import controlled from "./controlled";

export const fileUploadContent = {
usage,
multiple,
topbar,
fileItem,
customButtons,
buttonsContainer,
controlled,
};
15 changes: 15 additions & 0 deletions apps/docs/content/components/file-upload/multiple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const App = `import {FileUpload} from "@nextui-org/react";

export default function App() {
return (
<FileUpload multiple onChange={(files) => { /* handle files */}} />
);
}`;

const react = {
"/App.jsx": App,
};

export default {
...react,
};
15 changes: 15 additions & 0 deletions apps/docs/content/components/file-upload/topbar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const App = `import {FileUpload, FileUploadTopbar} from "@nextui-org/react";

export default function App() {
return (
<FileUpload topbar={<FileUploadTopbar maxAllowedSize="1 MB" />} />
);
}`;

const react = {
"/App.jsx": App,
};

export default {
...react,
};
15 changes: 15 additions & 0 deletions apps/docs/content/components/file-upload/usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const App = `import {FileUpload} from "@nextui-org/react";

export default function App() {
return (
<FileUpload onChange={(files) => { /* handle files */}} />
);
}`;

const react = {
"/App.jsx": App,
};

export default {
...react,
};
171 changes: 171 additions & 0 deletions apps/docs/content/docs/components/file-upload.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
title: "File Upload"
description: "File Upload adds/removes file(s) selected by user to upload on a server."
---

import {fileUploadContent} from "@/content/components/file-upload";

# File Upload

File Upload adds/removes file(s) selected by user to upload on a server.

<ComponentLinks component="file-upload" />

---


## Installation

<PackageManagers
showGlobalInstallWarning
commands={{
cli: "npx nextui-cli@latest add file-upload",
npm: "npm install @nextui-org/file-upload",
yarn: "yarn add @nextui-org/file-upload",
pnpm: "pnpm add @nextui-org/file-upload",
bun: "bun add @nextui-org/file-upload"
}}
/>

## Import

NextUI exports 3 fileupload-related components:

- **FileUpload**: The main component to display file(s) that is/are to be uploaded.
- **FileUploadItem**: The item component to display a single file item.
- **FileUploadTopbar**: The topbar component to display information like the number of files to upload, max file size, etc.

<ImportTabs
commands={{
main: 'import {FileUpload, FileUploadItem, FileUploadTopbar} from "@nextui-org/react";',
individual: 'import {FileUpload, FileUploadItem, FileUploadTopbar} from "@nextui-org/file-upload";',
}}
/>

## Usage

<CodeDemo title="Usage" files={fileUploadContent.usage} />

### Upload Multiple Items

If you set `multiple` to `true`, then the `FileUpload` will allow multiple files to select and upload.

<CodeDemo title="Upload Multiple Items" files={fileUploadContent.multiple} />

### With Topbar Section

If you want to show some information for the user, you can use your custom component or use the predefined `FileUploadTopbar` component.

<CodeDemo title="With Topbar Section" files={fileUploadContent.topbar} />

### With Custom File Item

There is a built-in component to display information regarding the selected files called `FileUploadItem`. You can implement your own

<CodeDemo title="With Custom Upload, Browse, Add and Reset Buttons" files={fileUploadContent.fileItem} />

### With Custom Upload, Browse, Add and Reset Buttons

If you need to have your custom buttons for browsing, adding or reseting, you can implement one for each.
In addition, there is an `uploadButton` prop for you in case uploading files prior to submitting the form is required.

<CodeDemo title="With Custom Upload, Browse, Add and Reset Buttons" files={fileUploadContent.customButtons} />

### With A Custom Container For Buttons Section

You can replace all the buttons entirely with your custom buttons container using the `buttons` prop.
It provides three handlers for adding a single file (`onAdd`), browsing (`onBrowse`) and removing all files (`onReset`).

<CodeDemo title="With Custom Buttons" files={fileUploadContent.buttonsContainer} />

### Controlled

In case there are some files that were previously uploaded on a server and one wants to show them as uploaded files,
the `files` prop can be set initially.

<CodeDemo title="Controlled" files={fileUploadContent.controlled} />

## Slots

- **base**: The main wrapper for the fileupload component. Modify the fileupload base styles.
- **items**: The class name of the fileupload items wrapper.
- **buttons**: The class name of the fileupload buttons wrapper.

## Data Attributes

`FileUpload` has the following attributes on the `base` element:

- **data-disabled**:
When the fileupload is disabled.

<Spacer y={4} />

## API

### FileUpload Props

| Attribute | Type | Description | Default |
| ------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------- |
| children | `ReactNode` \| `ReactNode[]` | The contents of the collection. Usually the array of `FileUploadItem` | |
| multiple | `boolean` | Whether multiple files can be selected from the device. | false |
| isDisabled | `boolean` | Whether the FileUpload items and buttons are disabled. | false |
| classNames | `Record<"base"| "items"| "buttons", string>` | Allows to set custom class names for the FileUpload slots. | - |
| files | `File[]` | Files as initial values or files to be controlled from outside of the FileUpload. | - |
| browseButtonText | `string` | Custom text for the default Browse Button. | - |
| browseButton | `ReactElement<ButtonProps>` | Custom button for browsing files. | - |
| addButton | `ReactElement<ButtonProps>` | Custom button for adding a single file. | - |
| resetButton | `ReactElement<ButtonProps>` | Custom button for reseting files to an empty array. | - |
| uploadButton | `ReactElement<ButtonProps>` | Custom button for uploading files to a server. | - |
| topbar | `ReactElement<HTMLElement>` | Custom topbar to show information regarding files. | - |
| buttons | `ReactElement<HTMLElement>` | Custom buttons for browsing, adding, .etc or any other ones that are needed for the user. | - |
| fileItemElement | `ReactElement<HTMLElement>` | Custom element for representing an item that is selected from the device. | - |

### FileUpload Events

| Attribute | Type | Description |
| ------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| onChange | `(files: File[]) => void` | Handler called when file(s) are selected from device or removed from the list (`onFileRemove` on an item should be called). |

### FileUpload Item Props

| Attribute | Type | Description | Default |
|---------------------------|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|---------|
| file | `File` | A file that is selected from the device. | |

### FileUpload Item Events

| Attribute | Type | Description |
| ------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| onFileRemove | `(name: string) => void` | Handler called when a file is removed from the list of items. |

### FileUpload Topbar Props

| Attribute | Type | Description | Default |
|---------------------------|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|---------|
| maxItems | `number` | Max number of items. | 1 |
| maxItemsText | `string` | Max number of items text. | |
| maxItemsElement | `ReactElement` | Custom Element to show Max number of items. | |
| maxAllowedSize | `FileSize` | Max file size allowed. | |
| maxAllowedSizeText | `string` | Max file size text. | "Max File Size" |
| maxAllowedSizeElement | `ReactElement` | Custom Element to show Max file size. | |
| totalMaxAllowedSize | `FileSize` | Total max size allowed for multiple files combined. | |
| totalMaxAllowedSizeText | `string` | Total max file size text. | "Total Max Files Size" |
| totalMaxAllowedSizeElement | `ReactElement` | Custom Element to show Total Max file size. | |

### FileSize

```ts
export type FileSize = `${number} KB` | `${number} MB`;
```

---

### FileUpload classNames

```ts
export type FileUploadClassnames = {
base?: string;
items?: string;
buttons?: string;
};
```
Loading