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 11 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
23 changes: 23 additions & 0 deletions apps/docs/content/components/file-upload/button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const App = `
import {FileUpload} from "@nextui-org/react";
import {SearchIcon, DeleteIcon} from "@nextui-org/shared-icons";

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 and consider code structure improvements.

The code is missing the Button import while using it in the component. Additionally, since this is documentation code, consider using a more maintainable structure.

Apply this diff to fix the issues:

const App = `
import {FileUpload} from "@nextui-org/react";
+import {Button} from "@nextui-org/react";
import {SearchIcon, DeleteIcon} from "@nextui-org/shared-icons";
📝 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";
import {SearchIcon, DeleteIcon} from "@nextui-org/shared-icons";
const App = `
import {FileUpload} from "@nextui-org/react";
import {Button} from "@nextui-org/react";
import {SearchIcon, DeleteIcon} from "@nextui-org/shared-icons";

export default function App() {
return (
<FileUpload
multiple
browseButton={<Button><SearchIcon /></Button>}
uploadButton={<Button onClick={() => alert("Files to upload are:\n" + files.map(file => file.name).join("\n"));}>Upload Files</Button>}
addButton={<Button>+</Button>}
resetButton={<Button><DeleteIcon /></Button>}
/>
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

Fix undefined files variable and improve user feedback.

The upload button's onClick handler references an undefined files variable, which will cause a runtime error.

Apply this diff to fix the issue:

    <FileUpload
      multiple
      browseButton={<Button><SearchIcon /></Button>}
-      uploadButton={<Button onClick={() => alert("Files to upload are:\n" + files.map(file => file.name).join("\n"));}>Upload Files</Button>}
+      uploadButton={
+        <Button 
+          onClick={(files) => {
+            if (files?.length) {
+              alert("Files to upload are:\n" + files.map(file => file.name).join("\n"));
+            }
+          }}
+        >
+          Upload Files
+        </Button>
+      }
      addButton={<Button>+</Button>}
      resetButton={<Button><DeleteIcon /></Button>}
    />

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


🛠️ Refactor suggestion

Consider enhancing the example with proper file handling.

The current example lacks important features that should be demonstrated in documentation:

  1. File type validation
  2. File size limits
  3. Proper error handling
  4. User-friendly feedback instead of alerts

Consider enhancing the example:

    <FileUpload
      multiple
+     accept=".jpg,.png,.pdf"
+     maxFileSize={5 * 1024 * 1024} // 5MB
      browseButton={<Button><SearchIcon /></Button>}
      uploadButton={
        <Button 
          onClick={(files) => {
            try {
              // Show proper upload feedback UI instead of alert
              console.log("Uploading files:", files);
            } catch (error) {
              console.error("Upload failed:", error);
            }
          }}
        >
          Upload Files
        </Button>
      }
+     onError={(error) => console.error("Error:", error)}
      addButton={<Button>+</Button>}
      resetButton={<Button><DeleteIcon /></Button>}
    />

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

);
}`;

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

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

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 statement.

The code uses the Button component but doesn't import it. Add the import to ensure the example is complete:

const App = `
import {FileUpload} from "@nextui-org/react";
+import {Button} from "@nextui-org/react";
import {SearchIcon, DeleteIcon} from "@nextui-org/shared-icons";
📝 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";
import {SearchIcon, DeleteIcon} from "@nextui-org/shared-icons";
const App = `
import {FileUpload} from "@nextui-org/react";
import {Button} from "@nextui-org/react";
import {SearchIcon, DeleteIcon} from "@nextui-org/shared-icons";

export default function App() {
return (
<FileUpload
multiple
buttons={(onBrowse, onAdd, onReset) => {
return <div>
<Button onClick={() => onBrowse()}><SearchIcon /></Button>
<Button onClick={() => onAdd()}>Add New File</Button>
<Button onClick={() => onReset()}>Remove All</Button>
</div>
}}
/>
);
}`;

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

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

const filesFromApi = async () => {
return [
new File([], "file1", {type: "jpg"}),
new File([], "file2", {type: "jpg"}),
new File([], "file3", {type: "jpg"}),
];
}
Arian94 marked this conversation as resolved.
Show resolved Hide resolved

export default function App() {
const [files, setFiles] = useState<File[]>();

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

return (
<FileUpload files={files} />
);
}`;
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

Improve error handling and loading states.

The component should handle loading and error states for better user experience.

Apply this diff to improve the implementation:

 export default function App() {
-  const [files, setFiles] = useState<File[]>();
+  const [files, setFiles] = useState<File[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<Error | null>(null);

   useEffect(() => {
-    filesFromApi().then(files => setFiles(files));
+    const fetchFiles = async () => {
+      setIsLoading(true);
+      try {
+        const result = await filesFromApi();
+        setFiles(result);
+      } catch (err) {
+        setError(err instanceof Error ? err : new Error('Failed to fetch files'));
+      } finally {
+        setIsLoading(false);
+      }
+    };
+    
+    fetchFiles();
   }, []);

   return (
-    <FileUpload files={files} />
+    <>
+      {error && <div>Error: {error.message}</div>}
+      <FileUpload 
+        files={files}
+        isDisabled={isLoading}
+      />
+    </>
   );
📝 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
export default function App() {
const [files, setFiles] = useState<File[]>();
useEffect(() => {
filesFromApi().then(files => setFiles(files));
}, []);
return (
<FileUpload files={files} />
);
}`;
export default function App() {
const [files, setFiles] = useState<File[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchFiles = async () => {
setIsLoading(true);
try {
const result = await filesFromApi();
setFiles(result);
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to fetch files'));
} finally {
setIsLoading(false);
}
};
fetchFiles();
}, []);
return (
<>
{error && <div>Error: {error.message}</div>}
<FileUpload
files={files}
isDisabled={isLoading}
/>
</>
);
}`;


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

export default {
...react,
};
19 changes: 19 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,19 @@
const App = `
import {FileUpload} from "@nextui-org/react";
import {SearchIcon, DeleteIcon} from "@nextui-org/shared-icons";

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 button from "./button";
import buttons from "./buttons";
import controlled from "./controlled";

export const fileUploadContent = {
usage,
multiple,
topbar,
fileItem,
button,
buttons,
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";

# Field Upload
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

Fix typo in the title: "Field Upload" should be "File Upload"

The title contains a typo that should be corrected to maintain consistency with the component name used throughout the documentation.

-# Field Upload
+# File Upload
📝 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
# Field 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

In case you need to upload files prior to submitting the form, you can implement a custom button. In addition, there are placeholders for
other buttons such as the Add Button to place your custom button (component) instead.

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

### 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.buttons} />

### 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. | - |
| browseButton | `ReactHTMLElement<HTMLButtonElement>` | Custom button for browsing files. | - |
| browseButtonText | `Record<"base"| "items"| "buttons, string>` | Custom text for the default Browse Button. | - |
| addButton | `ReactHTMLElement<HTMLButtonElement>` | Custom button for adding a single file. | - |
| resetButton | `ReactHTMLElement<HTMLButtonElement>` | Custom button for reseting files to an empty array. | - |
| uploadButton | `ReactHTMLElement<HTMLButtonElement>` | Custom button for uploading files to a server. | - |
| topbar | `Record<"base"| "items"| "buttons, string>` | Custom topbar to show information regarding files. | - |
| buttons | `Record<"base"| "items"| "buttons, string>` | Custom buttons for browsing, adding, .etc or any other ones that are needed for the user. | - |
| fileItemElement | `Record<"base"| "items"| "buttons, string>` | Custom element for representing an item that is selected from the device. | - |
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

Fix inconsistent and incorrect type definitions in props table

Several props have type definition issues that need to be addressed:

  1. The classNames prop has incorrect syntax in the type definition
  2. Several props are using the same incorrect type
  3. Some props are marked as HTML elements but should be React elements

Apply these corrections:

-| classNames                | `Record<"base"| "items"| "buttons, string>`   |
+| classNames                | `Record<"base" | "items" | "buttons", string>`  |

-| browseButton              | `ReactHTMLElement<HTMLButtonElement>`           |
+| browseButton              | `ReactElement`                                  |

-| browseButtonText          | `Record<"base"| "items"| "buttons, string>`   |
+| browseButtonText          | `string`                                        |

-| addButton                 | `ReactHTMLElement<HTMLButtonElement>`           |
+| addButton                 | `ReactElement`                                  |

-| resetButton               | `ReactHTMLElement<HTMLButtonElement>`           |
+| resetButton               | `ReactElement`                                  |

-| uploadButton              | `ReactHTMLElement<HTMLButtonElement>`           |
+| uploadButton              | `ReactElement`                                  |

-| topbar                    | `Record<"base"| "items"| "buttons, string>`   |
+| topbar                    | `ReactElement`                                  |

-| buttons                   | `Record<"base"| "items"| "buttons, string>`   |
+| buttons                   | `ReactElement`                                  |

-| fileItemElement           | `Record<"base"| "items"| "buttons, string>`   |
+| fileItemElement           | `ReactElement`                                  |
📝 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
| 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. | - |
| browseButton | `ReactHTMLElement<HTMLButtonElement>` | Custom button for browsing files. | - |
| browseButtonText | `Record<"base"| "items"| "buttons, string>` | Custom text for the default Browse Button. | - |
| addButton | `ReactHTMLElement<HTMLButtonElement>` | Custom button for adding a single file. | - |
| resetButton | `ReactHTMLElement<HTMLButtonElement>` | Custom button for reseting files to an empty array. | - |
| uploadButton | `ReactHTMLElement<HTMLButtonElement>` | Custom button for uploading files to a server. | - |
| topbar | `Record<"base"| "items"| "buttons, string>` | Custom topbar to show information regarding files. | - |
| buttons | `Record<"base"| "items"| "buttons, string>` | Custom buttons for browsing, adding, .etc or any other ones that are needed for the user. | - |
| fileItemElement | `Record<"base"| "items"| "buttons, string>` | Custom element for representing an item that is selected from the device. | - |
| 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. | - |
| browseButton | `ReactElement` | Custom button for browsing files. | - |
| browseButtonText | `string` | Custom text for the default Browse Button. | - |
| addButton | `ReactElement` | Custom button for adding a single file. | - |
| resetButton | `ReactElement` | Custom button for reseting files to an empty array. | - |
| uploadButton | `ReactElement` | Custom button for uploading files to a server. | - |
| topbar | `ReactElement` | Custom topbar to show information regarding files. | - |
| buttons | `ReactElement` | Custom buttons for browsing, adding, .etc or any other ones that are needed for the user. | - |
| fileItemElement | `ReactElement` | Custom element for representing an item that is selected from the device. | - |
🧰 Tools
🪛 LanguageTool

[style] ~113-~113: This phrase is redundant. Consider using “outside”.
Context: ...l values or files to be controlled from outside of the FileUpload. ...

(OUTSIDE_OF)


[style] ~120-~120: In American English, abbreviations like “etc.” require a period.
Context: ...| Custom buttons for browsing, adding, .etc or any other ones that are needed for t...

(ETC_PERIOD)


### 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. | |

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

Add essential documentation sections

The documentation would benefit from additional sections covering:

  1. Error Handling:

    • How to handle file validation errors
    • Error states and error messages
    • Example of error handling implementation
  2. Accessibility:

    • ARIA attributes
    • Keyboard navigation
    • Screen reader support
  3. Browser Compatibility:

    • Supported browsers
    • Known limitations
    • Fallback behavior

Would you like me to help generate content for these sections?

🧰 Tools
🪛 LanguageTool

[typographical] ~109-~109: Consider adding a comma after ‘Usually’ for more clarity.
Context: ... | The contents of the collection. Usually the array of FileUploadItem ...

(RB_LY_COMMA)


[style] ~113-~113: This phrase is redundant. Consider using “outside”.
Context: ...l values or files to be controlled from outside of the FileUpload. ...

(OUTSIDE_OF)


[style] ~120-~120: In American English, abbreviations like “etc.” require a period.
Context: ...| Custom buttons for browsing, adding, .etc or any other ones that are needed for t...

(ETC_PERIOD)

### FileSize

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

---

### FileUpload classNames

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