Skip to main content

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 Upload with beforeUpload intercepts the file and sends it to your own endpoint
  • getCredit renders the delete button at the bottom of each image (remember stopPropagation)

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

ApproachEffortGain
A · setUploadFuncKills 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

  1. CORS: your storage must serve images with proper CORS headers — otherwise Fabric taints the canvas and you can't export. ImagesGrid already sets crossOrigin="anonymous".
  2. Image dimensions: have your upload endpoint return width / height alongside the URL — saves a round-trip to measure the image in the browser.
  3. Large originals: for print/PDF, keep originals but show CDN-resized thumbnails. Use the original URL only when inserting into the canvas.

Next