fix: upload manager — URL imports, active count, board reset

1. URL imports now participate in upload manager pipeline:
   addUrlJob() creates a job row, uploadComplete/setFailed called
   on success/failure — consistent with local file uploads

2. activeCount only counts uploading/processing jobs, not failed.
   Failed jobs no longer inflate the "Uploads (n)" header count.

3. uploadManager.clear() called on boardId change so stale jobs
   from a previous board don't linger in the next board's panel.
This commit is contained in:
Hiren Kangad
2026-03-10 19:10:14 +05:30
parent 784554e40d
commit c126885e53
3 changed files with 39 additions and 3 deletions
+8
View File
@@ -119,14 +119,22 @@ export function setupDragDrop(
const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top); const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top);
const placeholder = createPlaceholder(viewport, world.x, world.y); const placeholder = createPlaceholder(viewport, world.x, world.y);
// Derive a filename from the URL for the upload manager
const urlFilename = url.split('/').pop()?.split('?')[0] || 'url-import';
const isVideoUrl = /\.(mp4|webm|mov)(\?|$)/i.test(url);
const jobId = uploads?.addUrlJob(urlFilename, isVideoUrl ? 'video' : 'image');
try { try {
const res = await uploadImageFromUrl(boardId, url); const res = await uploadImageFromUrl(boardId, url);
removePlaceholder(viewport, placeholder); removePlaceholder(viewport, placeholder);
const { id } = handleUploadResult(res, viewport, sceneManager, world.x, world.y, onChange); const { id } = handleUploadResult(res, viewport, sceneManager, world.x, world.y, onChange);
selection?.selectOnly(id); selection?.selectOnly(id);
const imgData = res.data.image || res.data;
if (jobId) uploads?.uploadComplete(jobId, imgData.id);
} catch (err: any) { } catch (err: any) {
console.error('URL image upload failed:', err); console.error('URL image upload failed:', err);
removePlaceholder(viewport, placeholder); removePlaceholder(viewport, placeholder);
if (jobId) uploads?.setFailed(jobId, err.response?.data?.error || err.message || 'URL import failed');
} }
} }
return; return;
+5
View File
@@ -64,6 +64,11 @@ export default function Editor({ isPublicView }: EditorProps) {
const canvasContainerRef = useRef<HTMLDivElement>(null); const canvasContainerRef = useRef<HTMLDivElement>(null);
const [uploadManager] = useState(() => new UploadManager()); const [uploadManager] = useState(() => new UploadManager());
// Reset upload manager when switching boards
useEffect(() => {
uploadManager.clear();
}, [boardId, uploadManager]);
// UI state // UI state
const [activeTool, setActiveTool] = useState<ToolType>(ToolType.SELECT); const [activeTool, setActiveTool] = useState<ToolType>(ToolType.SELECT);
const [color, setColor] = useState('#ffffff'); const [color, setColor] = useState('#ffffff');
+26 -3
View File
@@ -32,7 +32,7 @@ export class UploadManager {
for (const fn of this._listeners) fn(); for (const fn of this._listeners) fn();
} }
/** Create a new upload job. Returns the job ID. */ /** Create a new upload job from a local file. Returns the job ID. */
addJob(file: File, boardId: string): string { addJob(file: File, boardId: string): string {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const isVideo = file.type.startsWith('video/'); const isVideo = file.type.startsWith('video/');
@@ -51,6 +51,22 @@ export class UploadManager {
return id; return id;
} }
/** Create a job for a URL-based import (no File object, unknown size). */
addUrlJob(fileName: string, mediaType: 'image' | 'video'): string {
const id = crypto.randomUUID();
this.jobs.set(id, {
id,
fileName,
fileSize: 0,
mediaType,
status: 'uploading',
progress: 0,
createdAt: Date.now(),
});
this._notify();
return id;
}
/** Update upload progress (0-1). */ /** Update upload progress (0-1). */
setProgress(jobId: string, progress: number) { setProgress(jobId: string, progress: number) {
const job = this.jobs.get(jobId); const job = this.jobs.get(jobId);
@@ -116,6 +132,13 @@ export class UploadManager {
this._notify(); this._notify();
} }
/** Remove all jobs (used on board change). */
clear() {
for (const id of this._dismissTimers.keys()) this._clearDismissTimer(id);
this.jobs.clear();
this._notify();
}
/** Remove all completed/failed jobs. */ /** Remove all completed/failed jobs. */
clearFinished() { clearFinished() {
for (const [id, job] of this.jobs) { for (const [id, job] of this.jobs) {
@@ -127,11 +150,11 @@ export class UploadManager {
this._notify(); this._notify();
} }
/** Get active (non-done) job count. */ /** Get active (uploading or processing) job count. */
get activeCount(): number { get activeCount(): number {
let count = 0; let count = 0;
for (const job of this.jobs.values()) { for (const job of this.jobs.values()) {
if (job.status !== 'done') count++; if (job.status === 'uploading' || job.status === 'processing') count++;
} }
return count; return count;
} }