Skip to content

Commit 0e69fd4

Browse files
ezhil56xYousefEDmatthewlipski
authored
feat: added support for uppy (#842)
* feat: add support for uppy * feat: add support for uppy * feat: add support for uppy * feat: add support for uppy * feat: add support for uppy * feat: add support for uppy * feat: add support for uppy * feat: add support for uppy * feat: add support for uppy * feat: add support for uppy * feat: add support for uppy * feat: add support for uppy * update uppy integration * fix build * Made Uppy panel also appear for file replace button * Updated `examples.gen.tsx` * Updated `examples.gen.tsx` --------- Co-authored-by: yousefed <[email protected]> Co-authored-by: matthewlipski <[email protected]>
1 parent 4519fc8 commit 0e69fd4

File tree

14 files changed

+889
-12
lines changed

14 files changed

+889
-12
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"playground": true,
3+
"docs": true,
4+
"author": "ezhil56x",
5+
"tags": ["Intermediate", "Files"],
6+
"dependencies": {
7+
"@uppy/core": "^3.13.1",
8+
"@uppy/dashboard": "^3.9.1",
9+
"@uppy/drag-drop": "^3.1.1",
10+
"@uppy/file-input": "^3.1.2",
11+
"@uppy/image-editor": "^2.4.6",
12+
"@uppy/progress-bar": "^3.1.1",
13+
"@uppy/react": "^3.4.0",
14+
"@uppy/screen-capture": "^3.2.0",
15+
"@uppy/status-bar": "^3.1.1",
16+
"@uppy/webcam": "^3.4.2",
17+
"@uppy/xhr-upload": "^3.4.0",
18+
"react-icons": "^5.2.1"
19+
}
20+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import "@blocknote/core/fonts/inter.css";
2+
import { BlockNoteView } from "@blocknote/mantine";
3+
import "@blocknote/mantine/style.css";
4+
import {
5+
FilePanelController,
6+
FormattingToolbar,
7+
FormattingToolbarController,
8+
getFormattingToolbarItems,
9+
useCreateBlockNote,
10+
} from "@blocknote/react";
11+
12+
import { uploadFile, UppyFilePanel } from "./UppyFilePanel";
13+
import { FileReplaceButton } from "./FileReplaceButton";
14+
15+
export default function App() {
16+
// Creates a new editor instance.
17+
const editor = useCreateBlockNote({
18+
initialContent: [
19+
{
20+
type: "paragraph",
21+
content: "Welcome to this demo!",
22+
},
23+
{
24+
type: "paragraph",
25+
content: "Upload an image using the button below",
26+
},
27+
{
28+
type: "image",
29+
},
30+
{
31+
type: "paragraph",
32+
},
33+
],
34+
uploadFile,
35+
});
36+
37+
// Renders the editor instance using a React component.
38+
return (
39+
<BlockNoteView editor={editor} formattingToolbar={false} filePanel={false}>
40+
<FormattingToolbarController
41+
formattingToolbar={(props) => {
42+
// Replaces default file replace button with one that opens Uppy.
43+
const items = getFormattingToolbarItems();
44+
items.splice(2, 1, <FileReplaceButton key={"fileReplaceButton"} />);
45+
46+
return <FormattingToolbar {...props}>{items}</FormattingToolbar>;
47+
}}
48+
/>
49+
{/* Replaces default file panel with Uppy one. */}
50+
<FilePanelController filePanel={UppyFilePanel} />
51+
</BlockNoteView>
52+
);
53+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {
2+
BlockSchema,
3+
checkBlockIsFileBlock,
4+
InlineContentSchema,
5+
StyleSchema,
6+
} from "@blocknote/core";
7+
import {
8+
useBlockNoteEditor,
9+
useComponentsContext,
10+
useDictionary,
11+
useSelectedBlocks,
12+
} from "@blocknote/react";
13+
import { useEffect, useState } from "react";
14+
import { RiImageEditFill } from "react-icons/ri";
15+
import { UppyFilePanel } from "./UppyFilePanel";
16+
17+
// Copied with minor changes from:
18+
// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/FormattingToolbar/DefaultButtons/FileReplaceButton.tsx
19+
// Opens Uppy file panel instead of the default one.
20+
export const FileReplaceButton = () => {
21+
const dict = useDictionary();
22+
const Components = useComponentsContext()!;
23+
24+
const editor = useBlockNoteEditor<
25+
BlockSchema,
26+
InlineContentSchema,
27+
StyleSchema
28+
>();
29+
30+
const selectedBlocks = useSelectedBlocks(editor);
31+
32+
const [isOpen, setIsOpen] = useState<boolean>(false);
33+
34+
useEffect(() => {
35+
setIsOpen(false);
36+
}, [selectedBlocks]);
37+
38+
const block = selectedBlocks.length === 1 ? selectedBlocks[0] : undefined;
39+
40+
if (
41+
block === undefined ||
42+
!checkBlockIsFileBlock(block, editor) ||
43+
!editor.isEditable
44+
) {
45+
return null;
46+
}
47+
48+
return (
49+
<Components.Generic.Popover.Root opened={isOpen} position={"bottom"}>
50+
<Components.Generic.Popover.Trigger>
51+
<Components.FormattingToolbar.Button
52+
className={"bn-button"}
53+
onClick={() => setIsOpen(!isOpen)}
54+
isSelected={isOpen}
55+
mainTooltip={
56+
dict.formatting_toolbar.file_replace.tooltip[block.type] ||
57+
dict.formatting_toolbar.file_replace.tooltip["file"]
58+
}
59+
label={
60+
dict.formatting_toolbar.file_replace.tooltip[block.type] ||
61+
dict.formatting_toolbar.file_replace.tooltip["file"]
62+
}
63+
icon={<RiImageEditFill />}
64+
/>
65+
</Components.Generic.Popover.Trigger>
66+
<Components.Generic.Popover.Content
67+
className={"bn-popover-content bn-panel-popover"}
68+
variant={"panel-popover"}>
69+
{/* Replaces default file panel with our Uppy one. */}
70+
<UppyFilePanel block={block as any} />
71+
</Components.Generic.Popover.Content>
72+
</Components.Generic.Popover.Root>
73+
);
74+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Uploading Files with Uppy
2+
3+
This example allows users to upload files using [Uppy](https://uppy.io/) and use them in the editor.
4+
5+
Uppy is highly extensible and has an extensive ecosystem of plugins. For example, you can:
6+
7+
- record audio, screen or webcam
8+
- import files from Box / Dropbox / Facebook / Google Drive / Google Photos / Instagram / OneDrive / Zoom
9+
- select files from Unsplash
10+
- show an image editor (crop, rotate, etc)
11+
12+
(in this example, we've enabled the Webcam, ScreenCapture and Image Editor plugin)
13+
14+
**Try it out:** Click the "Add Image" button and you can either drop files or click "browse files" to upload them.
15+
16+
**Relevant Docs:**
17+
18+
- [Editor Setup](/docs/editor-basics/setup)
19+
- [Image](/docs/editor-basics/default-schema#image)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { FilePanelProps, useBlockNoteEditor } from "@blocknote/react";
2+
import Uppy, { UploadSuccessCallback } from "@uppy/core";
3+
import "@uppy/core/dist/style.min.css";
4+
import "@uppy/dashboard/dist/style.min.css";
5+
import { Dashboard } from "@uppy/react";
6+
import XHR from "@uppy/xhr-upload";
7+
import { useEffect } from "react";
8+
9+
// Image editor plugin
10+
import ImageEditor from "@uppy/image-editor";
11+
import "@uppy/image-editor/dist/style.min.css";
12+
13+
// Screen capture plugin
14+
import ScreenCapture from "@uppy/screen-capture";
15+
import "@uppy/screen-capture/dist/style.min.css";
16+
17+
// Webcam plugin
18+
import Webcam from "@uppy/webcam";
19+
import "@uppy/webcam/dist/style.min.css";
20+
21+
// Configure your Uppy instance here.
22+
const uppy = new Uppy()
23+
// Enabled plugins - you probably want to customize this
24+
// See https://uppy.io/examples/ for all the integrations like Google Drive,
25+
// Instagram Dropbox etc.
26+
.use(Webcam)
27+
.use(ScreenCapture)
28+
.use(ImageEditor)
29+
30+
// Uses an XHR upload plugin to upload files to tmpfiles.org.
31+
// You want to replace this with your own upload endpoint or Uppy Companion
32+
// server.
33+
.use(XHR, {
34+
endpoint: "https://tmpfiles.org/api/v1/upload",
35+
getResponseData(text, resp) {
36+
return {
37+
url: JSON.parse(text).data.url.replace(
38+
"tmpfiles.org/",
39+
"tmpfiles.org/dl/"
40+
),
41+
};
42+
},
43+
});
44+
45+
export function UppyFilePanel(props: FilePanelProps) {
46+
const { block } = props;
47+
const editor = useBlockNoteEditor();
48+
49+
useEffect(() => {
50+
// Listen for successful tippy uploads, and then update the Block with the
51+
// uploaded URL.
52+
const handler: UploadSuccessCallback<Record<string, unknown>> = (
53+
file,
54+
response
55+
) => {
56+
if (!file) {
57+
return;
58+
}
59+
60+
if (file.source === "uploadFile") {
61+
// Didn't originate from Dashboard, should be handled by `uploadFile`
62+
return;
63+
}
64+
if (response.status === 200) {
65+
const updateData = {
66+
props: {
67+
name: file?.name,
68+
url: response.uploadURL,
69+
},
70+
};
71+
editor.updateBlock(block, updateData);
72+
73+
// File should be removed from the Uppy instance after upload.
74+
uppy.removeFile(file.id);
75+
}
76+
};
77+
uppy.on("upload-success", handler);
78+
return () => {
79+
uppy.off("upload-success", handler);
80+
};
81+
}, [block, editor]);
82+
83+
// set up dashboard as in https://uppy.io/examples/
84+
return <Dashboard uppy={uppy} width={400} height={500} />;
85+
}
86+
87+
// Implementation for the BlockNote `uploadFile` function.
88+
// This function is used when for example, files are dropped into the editor.
89+
export async function uploadFile(file: File) {
90+
const id = uppy.addFile({
91+
id: file.name,
92+
name: file.name,
93+
type: file.type,
94+
data: file,
95+
source: "uploadFile",
96+
});
97+
98+
try {
99+
const result = await uppy.upload();
100+
return result.successful[0].response!.uploadURL!;
101+
} finally {
102+
uppy.removeFile(id);
103+
}
104+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<html lang="en">
2+
<head>
3+
<script>
4+
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
5+
</script>
6+
<meta charset="UTF-8" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>Uploading Files with Uppy</title>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="./main.tsx"></script>
13+
</body>
14+
</html>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
2+
import React from "react";
3+
import { createRoot } from "react-dom/client";
4+
import App from "./App";
5+
6+
const root = createRoot(document.getElementById("root")!);
7+
root.render(
8+
<React.StrictMode>
9+
<App />
10+
</React.StrictMode>
11+
);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "@blocknote/example-file-uploading-uppy",
3+
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
4+
"private": true,
5+
"version": "0.12.4",
6+
"scripts": {
7+
"start": "vite",
8+
"dev": "vite",
9+
"build": "tsc && vite build",
10+
"preview": "vite preview",
11+
"lint": "eslint . --max-warnings 0"
12+
},
13+
"dependencies": {
14+
"@blocknote/core": "latest",
15+
"@blocknote/react": "latest",
16+
"@blocknote/ariakit": "latest",
17+
"@blocknote/mantine": "latest",
18+
"@blocknote/shadcn": "latest",
19+
"react": "^18.3.1",
20+
"react-dom": "^18.3.1",
21+
"@uppy/core": "^3.13.1",
22+
"@uppy/dashboard": "^3.9.1",
23+
"@uppy/drag-drop": "^3.1.1",
24+
"@uppy/file-input": "^3.1.2",
25+
"@uppy/image-editor": "^2.4.6",
26+
"@uppy/progress-bar": "^3.1.1",
27+
"@uppy/react": "^3.4.0",
28+
"@uppy/screen-capture": "^3.2.0",
29+
"@uppy/status-bar": "^3.1.1",
30+
"@uppy/webcam": "^3.4.2",
31+
"@uppy/xhr-upload": "^3.4.0",
32+
"react-icons": "^5.2.1"
33+
},
34+
"devDependencies": {
35+
"@types/react": "^18.0.25",
36+
"@types/react-dom": "^18.0.9",
37+
"@vitejs/plugin-react": "^4.0.4",
38+
"eslint": "^8.10.0",
39+
"vite": "^4.4.8"
40+
},
41+
"eslintConfig": {
42+
"extends": [
43+
"../../../.eslintrc.js"
44+
]
45+
},
46+
"eslintIgnore": [
47+
"dist"
48+
]
49+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
3+
"compilerOptions": {
4+
"target": "ESNext",
5+
"useDefineForClassFields": true,
6+
"lib": [
7+
"DOM",
8+
"DOM.Iterable",
9+
"ESNext"
10+
],
11+
"allowJs": false,
12+
"skipLibCheck": true,
13+
"esModuleInterop": false,
14+
"allowSyntheticDefaultImports": true,
15+
"strict": true,
16+
"forceConsistentCasingInFileNames": true,
17+
"module": "ESNext",
18+
"moduleResolution": "Node",
19+
"resolveJsonModule": true,
20+
"isolatedModules": true,
21+
"noEmit": true,
22+
"jsx": "react-jsx",
23+
"composite": true
24+
},
25+
"include": [
26+
"."
27+
],
28+
"__ADD_FOR_LOCAL_DEV_references": [
29+
{
30+
"path": "../../../packages/core/"
31+
},
32+
{
33+
"path": "../../../packages/react/"
34+
}
35+
]
36+
}

0 commit comments

Comments
 (0)