Uppy File Panel
This example allows users to upload files using Uppy by replacing the default File Panel with an Uppy Dashboard.
Uppy is highly extensible and has an extensive ecosystem of plugins. For example, you can:
- Record audio, screen or webcam
- Import files from Box / Dropbox / Facebook / Google Drive / Google Photos / Instagram / OneDrive / Zoom
- Select files from Unsplash
- Show an image editor (crop, rotate, etc)
In this example, we've enabled the Webcam, ScreenCapture and Image Editor plugins.
Try it out: Click the "Add Image" button and you can either drop files or click "browse files" to upload them.
Relevant Docs:
import "@blocknote/core/fonts/inter.css";import { BlockNoteView } from "@blocknote/mantine";import "@blocknote/mantine/style.css";import { FilePanelController, FormattingToolbar, FormattingToolbarController, FormattingToolbarProps, getFormattingToolbarItems, useCreateBlockNote,} from "@blocknote/react";import { FileReplaceButton } from "./FileReplaceButton";import { uploadFile, UppyFilePanel } from "./UppyFilePanel";const CustomFormattingToolbar = (props: FormattingToolbarProps) => { // Replaces default file replace button with one that opens Uppy. const items = getFormattingToolbarItems(); items.splice( items.findIndex((c) => c.key === "replaceFileButton"), 1, <FileReplaceButton key={"fileReplaceButton"} />, ); return <FormattingToolbar {...props}>{items}</FormattingToolbar>;};export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ initialContent: [ { type: "paragraph", content: "Welcome to this demo!", }, { type: "paragraph", content: "Upload an image using the button below", }, { type: "image", }, ], uploadFile, }); // Renders the editor instance using a React component. return ( <BlockNoteView editor={editor} formattingToolbar={false} filePanel={false}> <FormattingToolbarController formattingToolbar={CustomFormattingToolbar} /> {/* Replaces default file panel with Uppy one. */} <FilePanelController filePanel={UppyFilePanel} /> </BlockNoteView> );}import { BlockSchema, blockHasType, InlineContentSchema, StyleSchema,} from "@blocknote/core";import { useBlockNoteEditor, useComponentsContext, useDictionary, useSelectedBlocks,} from "@blocknote/react";import { useEffect, useState } from "react";import { RiImageEditFill } from "react-icons/ri";import { UppyFilePanel } from "./UppyFilePanel";// Copied with minor changes from:// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/FormattingToolbar/DefaultButtons/FileReplaceButton.tsx// Opens Uppy file panel instead of the default one.export const FileReplaceButton = () => { const dict = useDictionary(); const Components = useComponentsContext()!; const editor = useBlockNoteEditor< BlockSchema, InlineContentSchema, StyleSchema >(); const selectedBlocks = useSelectedBlocks(editor); const [isOpen, setIsOpen] = useState<boolean>(false); useEffect(() => { setIsOpen(false); }, [selectedBlocks]); const block = selectedBlocks.length === 1 ? selectedBlocks[0] : undefined; if ( block === undefined || !blockHasType(block, editor, "file", { url: "string" }) || !editor.isEditable ) { return null; } return ( <Components.Generic.Popover.Root open={isOpen} position={"bottom"}> <Components.Generic.Popover.Trigger> <Components.FormattingToolbar.Button className={"bn-button"} onClick={() => setIsOpen(!isOpen)} isSelected={isOpen} mainTooltip={ dict.formatting_toolbar.file_replace.tooltip[block.type] || dict.formatting_toolbar.file_replace.tooltip["file"] } label={ dict.formatting_toolbar.file_replace.tooltip[block.type] || dict.formatting_toolbar.file_replace.tooltip["file"] } icon={<RiImageEditFill />} /> </Components.Generic.Popover.Trigger> <Components.Generic.Popover.Content className={"bn-popover-content bn-panel-popover"} variant={"panel-popover"} > {/* Replaces default file panel with our Uppy one. */} <UppyFilePanel blockId={block.id} /> </Components.Generic.Popover.Content> </Components.Generic.Popover.Root> );};import { uploadFile_DEV_ONLY } from "@blocknote/core";import { FilePanelProps, useBlockNoteEditor } from "@blocknote/react";import Uppy, { UploadCompleteCallback } from "@uppy/core";import "@uppy/core/dist/style.min.css";import "@uppy/dashboard/dist/style.min.css";import { Dashboard } from "@uppy/react";import { useEffect } from "react";// Image editor pluginimport ImageEditor from "@uppy/image-editor";import "@uppy/image-editor/dist/style.min.css";// Screen capture pluginimport ScreenCapture from "@uppy/screen-capture";import "@uppy/screen-capture/dist/style.min.css";// Webcam pluginimport Webcam from "@uppy/webcam";import "@uppy/webcam/dist/style.min.css";// Configure your Uppy instance here.const uppy = new Uppy() // Enabled plugins - you probably want to customize this // See https://uppy.io/examples/ for all the integrations like Google Drive, // Instagram Dropbox etc. .use(Webcam) .use(ScreenCapture) .use(ImageEditor);// No uploader plugin is registered: for this demo we "upload" files with// BlockNote's dev-only helper, which encodes them as base64 data URLs. In a real// app you'd add an uploader like `@uppy/xhr-upload` pointing at your own backend// or Uppy Companion server.export function UppyFilePanel(props: FilePanelProps) { const { blockId } = props; const editor = useBlockNoteEditor(); useEffect(() => { // Listen for completed Dashboard uploads, then update the Block with the // uploaded file's URL. const handler: UploadCompleteCallback<Record<string, unknown>> = async ( result, ) => { for (const file of result.successful) { editor.updateBlock(blockId, { props: { name: file.name, url: await uploadFile_DEV_ONLY(file.data as File), }, }); // File should be removed from the Uppy instance after upload. uppy.removeFile(file.id); } }; uppy.on("complete", handler); return () => { uppy.off("complete", handler); }; }, [blockId, editor]); // set up dashboard as in https://uppy.io/examples/ return <Dashboard uppy={uppy} width={400} height={500} />;}// Implementation for the BlockNote `uploadFile` function.// This function is used when for example, files are dropped into the editor.export async function uploadFile(file: File) { return uploadFile_DEV_ONLY(file);}/// <reference types="vite-plus/client" />