Upload Panel
The built-in UploadPanel works, but its default behavior is deliberately minimal — it inlines each selected local file as a base64 string directly into the canvas JSON. The downsides are obvious:
- JSON payload explodes (a 2MB image easily pushes the JSON past 3MB)
- No persistence across sessions (refresh loses everything)
- No cross-device sync
- No delete / favorite / categorize
In production you really want to wire up your own object storage (S3 / OSS / COS / a home-grown image server). This page covers two levels of improvement.
Approach A: minimal — setUploadFunc
Simplest change: keep the built-in UI, only swap the upload logic. Canvas JSON ends up with short URLs instead of base64 blobs.
import { setUploadFunc } from '@ydesign/react-editor/side-panel/upload-panel';
setUploadFunc(async (file: File) => {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('https://your-api.com/upload', {
method: 'POST',
body: formData,
});
const { url } = await res.json();
// Must return a usable URL string
return url;
});
Signature: (file: File) => Promise<string> — take a File, return a URL.
Use this when:
- You just want to fix the "no base64" problem, keeping the default UX
- You don't need history / search / delete features
Approach B: replace the whole UploadSection
If you want a real asset-library experience (history, search, delete, favorite), replace the whole Section:
import { useEffect, useState, useRef } from 'react';
import { observer } from 'mobx-react-lite';
import { Button, Upload } from 'antd';
import { Plus, Trash } from 'lucide-react';
import {
SectionTab,
DEFAULT_SECTIONS,
SidePanel,
} from '@ydesign/react-editor/side-panel';
import { ImagesGrid } from '@ydesign/react-editor/side-panel/images-grid';
import type { Section } from '@ydesign/react-editor/side-panel';
type Asset = {
id: string;
url: string;
thumbnail: string;
width: number;
height: number;
};
const MyUploadSection: Section = {
name: 'upload', // 👈 same name overrides the built-in
Tab: observer(props => (
<SectionTab name="My Uploads" {...props}>
<Plus size={20} />
</SectionTab>
)),
Panel: observer(({ store }) => {
const [assets, setAssets] = useState<Asset[]>([]);
const [loading, setLoading] = useState(false);
const loadAssets = async () => {
const res = await fetch('/api/assets').then(r => r.json());
setAssets(res.list);
};
useEffect(() => {
loadAssets();
}, []);
const handleUpload = async (file: File) => {
setLoading(true);
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
}).then(r => r.json());
setAssets(prev => [res, ...prev]);
} finally {
setLoading(false);
}
return false; // prevent Antd's default upload
};
const handleDelete = async (id: string) => {
await fetch(`/api/assets/${id}`, { method: 'DELETE' });
setAssets(prev => prev.filter(a => a.id !== id));
};
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Upload accept="image/*" beforeUpload={handleUpload} showUploadList={false}>
<Button icon={<Plus size={14} />} loading={loading} block>
Upload image
</Button>
</Upload>
<div style={{ flex: 1, marginTop: 12, overflow: 'auto' }}>
<ImagesGrid
images={assets}
isLoading={loading}
getPreview={a => a.thumbnail}
getCredit={a => (
<Trash
size={12}
onClick={e => {
e.stopPropagation();
handleDelete(a.id);
}}
/>
)}
onSelect={(a, pos) => {
store.addElement({
type: 'image',
src: a.url,
left: pos?.x ?? 50,
top: pos?.y ?? 50,
width: a.width,
height: a.height,
});
}}
/>
</div>
</div>
);
}),
};
const sections = DEFAULT_SECTIONS.map(s =>
s.name === 'upload' ? MyUploadSection : s
);
<SidePanel store={store} sections={sections} />;
Highlights:
- Reuse
<ImagesGrid />to render history - Antd
UploadwithbeforeUploadintercepts the file and sends it to your own endpoint getCreditrenders the delete button at the bottom of each image (rememberstopPropagation)
Approach C: override the default upload API (pair with A or B)
@ydesign/react-editor exposes an uploadImage API key that any internal code path uses for "upload an image". Override it via setAPI:
import { setAPI, setBaseURL } from '@ydesign/react-editor';
setBaseURL('https://api.your-company.com');
setAPI('uploadImage', () => ({
method: 'POST',
url: 'https://your-api.com/assets/upload',
}));
This way every other "upload image" path (auto-converting base64 on export, inpaint output, etc.) also goes through your endpoint.
Comparison
| Approach | Effort | Gain |
|---|---|---|
A · setUploadFunc | ⭐ | Kills the base64 bloat |
| B · replace the whole Section | ⭐⭐⭐ | Full asset-library UX (history / delete / search / categorize) |
C · setAPI('uploadImage', …) | ⭐ | Unifies where "uploads" go across the app |
For production, combine A + C; move to B when asset management becomes a priority.
Gotchas
- CORS: your storage must serve images with proper CORS headers — otherwise Fabric taints the canvas and you can't export.
ImagesGridalready setscrossOrigin="anonymous". - Image dimensions: have your upload endpoint return
width/heightalongside the URL — saves a round-trip to measure the image in the browser. - Large originals: for print/PDF, keep originals but show CDN-resized thumbnails. Use the original URL only when inserting into the canvas.