feat: auto-select imported items after upload

Pass SelectionManager to setupDragDrop and setupPaste. After each
upload completes, the newly created scene items are selected:
- Single file/URL: selectOnly
- Multi-file drop/paste: select all new items
This commit is contained in:
Hiren Kangad
2026-03-10 18:51:28 +05:30
parent 3c6a5bf29e
commit 26527006a6
2 changed files with 37 additions and 11 deletions
+35 -9
View File
@@ -1,6 +1,7 @@
import { Graphics } from 'pixi.js'; import { Graphics } from 'pixi.js';
import type { Viewport } from 'pixi-viewport'; import type { Viewport } from 'pixi-viewport';
import type { SceneManager } from './SceneManager'; import type { SceneManager } from './SceneManager';
import type { SelectionManager } from './SelectionManager';
import { uploadImage, uploadImageFromUrl } from '../api'; import { uploadImage, uploadImageFromUrl } from '../api';
type OnChange = () => void; type OnChange = () => void;
@@ -29,7 +30,7 @@ function removePlaceholder(viewport: Viewport, g: Graphics) {
/* Handle upload response (image or video) */ /* Handle upload response (image or video) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/** Returns the placed dimensions so callers can offset subsequent items. */ /** Returns the placed dimensions and item ID so callers can offset subsequent items and select them. */
function handleUploadResult( function handleUploadResult(
res: any, res: any,
viewport: Viewport, viewport: Viewport,
@@ -37,7 +38,7 @@ function handleUploadResult(
x: number, x: number,
y: number, y: number,
onChange: OnChange, onChange: OnChange,
): { w: number; h: number } { ): { w: number; h: number; id: string } {
const imgData = res.data.image || res.data; const imgData = res.data.image || res.data;
const mediaType: string = imgData.media_type || 'image'; const mediaType: string = imgData.media_type || 'image';
const assetKey: string | undefined = imgData.asset_key; const assetKey: string | undefined = imgData.asset_key;
@@ -54,20 +55,21 @@ function handleUploadResult(
finalH = Math.round(h * scale); finalH = Math.round(h * scale);
} }
let item;
if (mediaType === 'video' && assetKey) { if (mediaType === 'video' && assetKey) {
const posterKey: string | undefined = imgData.poster_asset_key; const posterKey: string | undefined = imgData.poster_asset_key;
const duration: number | undefined = imgData.duration; const duration: number | undefined = imgData.duration;
sceneManager.addVideoFromUpload(assetKey, finalW, finalH, x, y, posterKey, duration); item = sceneManager.addVideoFromUpload(assetKey, finalW, finalH, x, y, posterKey, duration);
} else if (assetKey) { } else if (assetKey) {
sceneManager.addImageFromUpload(assetKey, finalW, finalH, x, y); item = sceneManager.addImageFromUpload(assetKey, finalW, finalH, x, y);
} else { } else {
// Fallback: legacy response without asset_key — use addImageFromUpload with public_url as key // Fallback: legacy response without asset_key — use addImageFromUpload with public_url as key
const fallbackKey = imgData.public_url || imgData.id || 'unknown'; const fallbackKey = imgData.public_url || imgData.id || 'unknown';
sceneManager.addImageFromUpload(fallbackKey, finalW, finalH, x, y); item = sceneManager.addImageFromUpload(fallbackKey, finalW, finalH, x, y);
} }
onChange(); onChange();
return { w: finalW, h: finalH }; return { w: finalW, h: finalH, id: item.id };
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -80,6 +82,7 @@ export function setupDragDrop(
sceneManager: SceneManager, sceneManager: SceneManager,
boardId: string, boardId: string,
onChange: OnChange, onChange: OnChange,
selection?: SelectionManager | null,
): () => void { ): () => void {
function onDragOver(e: DragEvent) { function onDragOver(e: DragEvent) {
e.preventDefault(); e.preventDefault();
@@ -113,7 +116,8 @@ export function setupDragDrop(
try { try {
const res = await uploadImageFromUrl(boardId, url); const res = await uploadImageFromUrl(boardId, url);
removePlaceholder(viewport, placeholder); removePlaceholder(viewport, placeholder);
handleUploadResult(res, viewport, sceneManager, world.x, world.y, onChange); const { id } = handleUploadResult(res, viewport, sceneManager, world.x, world.y, onChange);
selection?.selectOnly(id);
} catch (err) { } catch (err) {
console.error('URL image upload failed:', err); console.error('URL image upload failed:', err);
removePlaceholder(viewport, placeholder); removePlaceholder(viewport, placeholder);
@@ -137,6 +141,7 @@ export function setupDragDrop(
let rowMaxH = 0; let rowMaxH = 0;
let cursorY = world.y; let cursorY = world.y;
const colWidths: number[] = new Array(cols).fill(0); // track widest per column const colWidths: number[] = new Array(cols).fill(0); // track widest per column
const newItemIds: string[] = [];
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = files[i]; const file = files[i];
@@ -151,7 +156,8 @@ export function setupDragDrop(
try { try {
const res = await uploadImage(boardId, file); const res = await uploadImage(boardId, file);
removePlaceholder(viewport, placeholder); removePlaceholder(viewport, placeholder);
const { w: placedW, h: placedH } = handleUploadResult(res, viewport, sceneManager, cursorX, cursorY, onChange); const { w: placedW, h: placedH, id } = handleUploadResult(res, viewport, sceneManager, cursorX, cursorY, onChange);
newItemIds.push(id);
if (placedW > (colWidths[col] || 0)) colWidths[col] = placedW; if (placedW > (colWidths[col] || 0)) colWidths[col] = placedW;
if (placedH > rowMaxH) rowMaxH = placedH; if (placedH > rowMaxH) rowMaxH = placedH;
} catch (err) { } catch (err) {
@@ -169,6 +175,14 @@ export function setupDragDrop(
rowMaxH = 0; rowMaxH = 0;
} }
} }
// Select all newly imported items
if (selection && newItemIds.length > 0) {
selection.selectOnly(newItemIds[0]);
for (let i = 1; i < newItemIds.length; i++) {
selection.toggle(newItemIds[i]);
}
}
} }
// Prevent browser default (opening file in new tab) on the whole document // Prevent browser default (opening file in new tab) on the whole document
@@ -201,11 +215,14 @@ export function setupPaste(
sceneManager: SceneManager, sceneManager: SceneManager,
boardId: string, boardId: string,
onChange: OnChange, onChange: OnChange,
selection?: SelectionManager | null,
): () => void { ): () => void {
async function onPaste(e: ClipboardEvent) { async function onPaste(e: ClipboardEvent) {
const items = e.clipboardData?.items; const items = e.clipboardData?.items;
if (!items) return; if (!items) return;
const newItemIds: string[] = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i]; const item = items[i];
if (!item.type.startsWith('image/') && !item.type.startsWith('video/')) continue; if (!item.type.startsWith('image/') && !item.type.startsWith('video/')) continue;
@@ -224,12 +241,21 @@ export function setupPaste(
try { try {
const res = await uploadImage(boardId, file); const res = await uploadImage(boardId, file);
removePlaceholder(viewport, placeholder); removePlaceholder(viewport, placeholder);
handleUploadResult(res, viewport, sceneManager, cx - 100, cy - 75, onChange); const { id } = handleUploadResult(res, viewport, sceneManager, cx - 100, cy - 75, onChange);
newItemIds.push(id);
} catch (err) { } catch (err) {
console.error('Image paste upload failed:', err); console.error('Image paste upload failed:', err);
removePlaceholder(viewport, placeholder); removePlaceholder(viewport, placeholder);
} }
} }
// Select all pasted items
if (selection && newItemIds.length > 0) {
selection.selectOnly(newItemIds[0]);
for (let i = 1; i < newItemIds.length; i++) {
selection.toggle(newItemIds[i]);
}
}
} }
document.addEventListener('paste', onPaste); document.addEventListener('paste', onPaste);
+2 -2
View File
@@ -296,9 +296,9 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
dropTarget = canvasEl?.parentElement ?? null; dropTarget = canvasEl?.parentElement ?? null;
} }
if (dropTarget) { if (dropTarget) {
dropCleanupRef.current = setupDragDrop(dropTarget, viewport, scene, resolvedBoardId, onCanvasChange); dropCleanupRef.current = setupDragDrop(dropTarget, viewport, scene, resolvedBoardId, onCanvasChange, selection);
} }
pasteCleanupRef.current = setupPaste(viewport, scene, resolvedBoardId, onCanvasChange); pasteCleanupRef.current = setupPaste(viewport, scene, resolvedBoardId, onCanvasChange, selection);
} }
}, 200); }, 200);